@byline/host-tanstack-start 5.0.0 → 5.1.1
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/dist/admin-shell/chrome/admin-app-bar.d.ts +7 -0
- package/dist/admin-shell/chrome/admin-app-bar.js +10 -2
- package/dist/admin-shell/chrome/optional-i18n-provider.d.ts +29 -0
- package/dist/admin-shell/chrome/optional-i18n-provider.js +24 -0
- package/dist/admin-shell/chrome/session-change-boundary.d.ts +14 -0
- package/dist/admin-shell/chrome/session-change-boundary.js +119 -0
- package/dist/admin-shell/chrome/session-change-boundary.module.js +11 -0
- package/dist/admin-shell/chrome/session-change-boundary.test.d.ts +8 -0
- package/dist/admin-shell/chrome/session-change-boundary_module.css +81 -0
- package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -1
- package/dist/admin-shell/chrome/sign-in-page.js +60 -3
- package/dist/integrations/byline-admin-services.js +7 -1
- package/dist/integrations/client-ip.d.ts +19 -0
- package/dist/integrations/client-ip.js +34 -0
- package/dist/integrations/session-coordination.d.ts +24 -0
- package/dist/integrations/session-coordination.js +85 -0
- package/dist/integrations/session-middleware.d.ts +11 -0
- package/dist/integrations/session-middleware.js +94 -0
- package/dist/integrations/session-request-kind.d.ts +9 -0
- package/dist/integrations/session-request-kind.js +10 -0
- package/dist/integrations/session-request-middleware.d.ts +9 -0
- package/dist/integrations/session-request-middleware.js +7 -0
- package/dist/integrations/sign-in-body.d.ts +12 -0
- package/dist/integrations/sign-in-body.js +52 -0
- package/dist/integrations/sign-in-middleware.d.ts +9 -0
- package/dist/integrations/sign-in-middleware.js +10 -0
- package/dist/routes/create-admin-layout-route.js +46 -31
- package/dist/routes/create-sign-in-route.js +4 -2
- package/dist/server-fns/admin-account/change-password.d.ts +3 -1
- package/dist/server-fns/admin-account/change-password.js +9 -3
- package/dist/server-fns/admin-account/get.d.ts +3 -1
- package/dist/server-fns/admin-account/get.js +4 -1
- package/dist/server-fns/admin-account/index.d.ts +5 -5
- package/dist/server-fns/admin-account/update.d.ts +3 -1
- package/dist/server-fns/admin-account/update.js +4 -1
- package/dist/server-fns/admin-activity/get.d.ts +3 -1
- package/dist/server-fns/admin-activity/get.js +4 -1
- package/dist/server-fns/admin-permissions/get-role-abilities.d.ts +3 -1
- package/dist/server-fns/admin-permissions/get-role-abilities.js +4 -1
- package/dist/server-fns/admin-permissions/index.d.ts +4 -4
- package/dist/server-fns/admin-permissions/list-registered.d.ts +3 -1
- package/dist/server-fns/admin-permissions/list-registered.js +4 -1
- package/dist/server-fns/admin-permissions/set-role-abilities.d.ts +3 -1
- package/dist/server-fns/admin-permissions/set-role-abilities.js +4 -1
- package/dist/server-fns/admin-permissions/who-has.d.ts +3 -1
- package/dist/server-fns/admin-permissions/who-has.js +4 -1
- package/dist/server-fns/admin-roles/create.d.ts +3 -1
- package/dist/server-fns/admin-roles/create.js +4 -1
- package/dist/server-fns/admin-roles/delete.d.ts +3 -1
- package/dist/server-fns/admin-roles/delete.js +4 -1
- package/dist/server-fns/admin-roles/get.d.ts +3 -1
- package/dist/server-fns/admin-roles/get.js +4 -1
- package/dist/server-fns/admin-roles/index.d.ts +6 -6
- package/dist/server-fns/admin-roles/list.d.ts +3 -1
- package/dist/server-fns/admin-roles/list.js +4 -1
- package/dist/server-fns/admin-roles/reorder.d.ts +3 -1
- package/dist/server-fns/admin-roles/reorder.js +4 -1
- package/dist/server-fns/admin-roles/update.d.ts +3 -1
- package/dist/server-fns/admin-roles/update.js +4 -1
- package/dist/server-fns/admin-users/create.d.ts +3 -1
- package/dist/server-fns/admin-users/create.js +4 -1
- package/dist/server-fns/admin-users/delete.d.ts +3 -1
- package/dist/server-fns/admin-users/delete.js +4 -1
- package/dist/server-fns/admin-users/disable.d.ts +3 -1
- package/dist/server-fns/admin-users/disable.js +4 -1
- package/dist/server-fns/admin-users/enable.d.ts +3 -1
- package/dist/server-fns/admin-users/enable.js +4 -1
- package/dist/server-fns/admin-users/get-user-roles.d.ts +3 -1
- package/dist/server-fns/admin-users/get-user-roles.js +4 -1
- package/dist/server-fns/admin-users/get.d.ts +3 -1
- package/dist/server-fns/admin-users/get.js +4 -1
- package/dist/server-fns/admin-users/index.d.ts +10 -10
- package/dist/server-fns/admin-users/list.d.ts +3 -1
- package/dist/server-fns/admin-users/list.js +4 -1
- package/dist/server-fns/admin-users/set-password.d.ts +3 -1
- package/dist/server-fns/admin-users/set-password.js +4 -1
- package/dist/server-fns/admin-users/set-user-roles.d.ts +3 -1
- package/dist/server-fns/admin-users/set-user-roles.js +4 -1
- package/dist/server-fns/admin-users/update.d.ts +3 -1
- package/dist/server-fns/admin-users/update.js +4 -1
- package/dist/server-fns/ai/execute.d.ts +3 -1
- package/dist/server-fns/ai/execute.js +4 -1
- package/dist/server-fns/ai/index.d.ts +1 -1
- package/dist/server-fns/analytics/maintenance.d.ts +6 -2
- package/dist/server-fns/analytics/maintenance.js +7 -2
- package/dist/server-fns/analytics/queries.d.ts +18 -6
- package/dist/server-fns/analytics/queries.js +19 -6
- package/dist/server-fns/auth/current-user.d.ts +1 -0
- package/dist/server-fns/auth/current-user.js +5 -2
- package/dist/server-fns/auth/renew.d.ts +13 -0
- package/dist/server-fns/auth/renew.js +45 -0
- package/dist/server-fns/auth/sign-in.d.ts +2 -1
- package/dist/server-fns/auth/sign-in.js +88 -17
- package/dist/server-fns/auth/sign-out.d.ts +3 -1
- package/dist/server-fns/auth/sign-out.js +38 -6
- package/dist/server-fns/collections/audit.d.ts +3 -1
- package/dist/server-fns/collections/audit.js +4 -1
- package/dist/server-fns/collections/copy-to-locale.d.ts +10 -1
- package/dist/server-fns/collections/copy-to-locale.js +5 -2
- package/dist/server-fns/collections/create.d.ts +10 -1
- package/dist/server-fns/collections/create.js +5 -2
- package/dist/server-fns/collections/delete-locale.d.ts +10 -1
- package/dist/server-fns/collections/delete-locale.js +5 -2
- package/dist/server-fns/collections/delete.d.ts +10 -1
- package/dist/server-fns/collections/delete.js +5 -2
- package/dist/server-fns/collections/duplicate.d.ts +10 -1
- package/dist/server-fns/collections/duplicate.js +5 -2
- package/dist/server-fns/collections/get.d.ts +7 -0
- package/dist/server-fns/collections/get.js +8 -3
- package/dist/server-fns/collections/history.d.ts +3 -1
- package/dist/server-fns/collections/history.js +4 -1
- package/dist/server-fns/collections/index.d.ts +21 -21
- package/dist/server-fns/collections/list.d.ts +10 -1
- package/dist/server-fns/collections/list.js +5 -2
- package/dist/server-fns/collections/reindex.d.ts +3 -1
- package/dist/server-fns/collections/reindex.js +4 -1
- package/dist/server-fns/collections/reorder.d.ts +10 -1
- package/dist/server-fns/collections/reorder.js +5 -2
- package/dist/server-fns/collections/restore-version.d.ts +10 -1
- package/dist/server-fns/collections/restore-version.js +5 -2
- package/dist/server-fns/collections/scheduled-publication.d.ts +22 -5
- package/dist/server-fns/collections/scheduled-publication.js +17 -6
- package/dist/server-fns/collections/set-list-view-preference.d.ts +3 -1
- package/dist/server-fns/collections/set-list-view-preference.js +4 -1
- package/dist/server-fns/collections/stats.js +4 -1
- package/dist/server-fns/collections/status.d.ts +13 -2
- package/dist/server-fns/collections/status.js +8 -3
- package/dist/server-fns/collections/tree.d.ts +22 -5
- package/dist/server-fns/collections/tree.js +17 -6
- package/dist/server-fns/collections/update.d.ts +13 -2
- package/dist/server-fns/collections/update.js +8 -3
- package/dist/server-fns/collections/upload.d.ts +10 -1
- package/dist/server-fns/collections/upload.js +4 -1
- package/dist/server-fns/i18n/set-locale.d.ts +3 -1
- package/dist/server-fns/i18n/set-locale.js +18 -3
- package/dist/server-fns/preview/disable.d.ts +3 -1
- package/dist/server-fns/preview/disable.js +4 -1
- package/dist/server-fns/preview/enable.d.ts +3 -1
- package/dist/server-fns/preview/enable.js +4 -1
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +10 -1
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +5 -2
- package/dist/server-fns/singletons/change-status.d.ts +10 -1
- package/dist/server-fns/singletons/change-status.js +5 -2
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +10 -1
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +5 -2
- package/dist/server-fns/singletons/copy-to-locale.d.ts +10 -1
- package/dist/server-fns/singletons/copy-to-locale.js +5 -2
- package/dist/server-fns/singletons/find-by-version.d.ts +3 -1
- package/dist/server-fns/singletons/find-by-version.js +4 -1
- package/dist/server-fns/singletons/get-scheduled-publish.d.ts +3 -1
- package/dist/server-fns/singletons/get-scheduled-publish.js +4 -1
- package/dist/server-fns/singletons/get.d.ts +3 -1
- package/dist/server-fns/singletons/get.js +4 -1
- package/dist/server-fns/singletons/history.d.ts +3 -1
- package/dist/server-fns/singletons/history.js +4 -1
- package/dist/server-fns/singletons/restore-version.d.ts +10 -1
- package/dist/server-fns/singletons/restore-version.js +5 -2
- package/dist/server-fns/singletons/schedule-publish.d.ts +10 -1
- package/dist/server-fns/singletons/schedule-publish.js +5 -2
- package/dist/server-fns/singletons/unpublish.d.ts +10 -1
- package/dist/server-fns/singletons/unpublish.js +5 -2
- package/dist/server-fns/singletons/update.d.ts +10 -1
- package/dist/server-fns/singletons/update.js +5 -2
- package/package.json +19 -19
- package/src/admin-shell/chrome/admin-app-bar.tsx +11 -6
- package/src/admin-shell/chrome/optional-i18n-provider.tsx +61 -0
- package/src/admin-shell/chrome/session-change-boundary.module.css +112 -0
- package/src/admin-shell/chrome/session-change-boundary.test.tsx +91 -0
- package/src/admin-shell/chrome/session-change-boundary.tsx +155 -0
- package/src/admin-shell/chrome/sign-in-page.tsx +68 -3
- package/src/integrations/byline-admin-services.ts +7 -1
- package/src/integrations/client-ip.test.node.ts +45 -0
- package/src/integrations/client-ip.ts +54 -0
- package/src/integrations/framework-fetch-shape.test.node.ts +56 -0
- package/src/integrations/session-client.test.node.ts +121 -0
- package/src/integrations/session-coordination.test.node.ts +77 -0
- package/src/integrations/session-coordination.ts +96 -0
- package/src/integrations/session-middleware.test.node.ts +87 -0
- package/src/integrations/session-middleware.ts +107 -0
- package/src/integrations/session-request-kind.ts +18 -0
- package/src/integrations/session-request-middleware.ts +17 -0
- package/src/integrations/sign-in-body.test.node.ts +59 -0
- package/src/integrations/sign-in-body.ts +50 -0
- package/src/integrations/sign-in-middleware.ts +24 -0
- package/src/routes/create-admin-layout-route.tsx +48 -30
- package/src/routes/create-sign-in-route.tsx +10 -2
- package/src/server-fns/admin-account/change-password.test.node.ts +69 -0
- package/src/server-fns/admin-account/change-password.ts +13 -2
- package/src/server-fns/admin-account/get.ts +2 -0
- package/src/server-fns/admin-account/update.ts +2 -0
- package/src/server-fns/admin-activity/get.ts +2 -0
- package/src/server-fns/admin-permissions/get-role-abilities.ts +2 -0
- package/src/server-fns/admin-permissions/list-registered.ts +5 -4
- package/src/server-fns/admin-permissions/set-role-abilities.ts +2 -0
- package/src/server-fns/admin-permissions/who-has.ts +2 -0
- package/src/server-fns/admin-roles/create.ts +2 -0
- package/src/server-fns/admin-roles/delete.ts +2 -0
- package/src/server-fns/admin-roles/get.ts +2 -0
- package/src/server-fns/admin-roles/list.ts +5 -4
- package/src/server-fns/admin-roles/reorder.ts +2 -0
- package/src/server-fns/admin-roles/update.ts +2 -0
- package/src/server-fns/admin-users/create.ts +2 -0
- package/src/server-fns/admin-users/delete.ts +2 -0
- package/src/server-fns/admin-users/disable.ts +2 -0
- package/src/server-fns/admin-users/enable.ts +2 -0
- package/src/server-fns/admin-users/get-user-roles.ts +2 -0
- package/src/server-fns/admin-users/get.ts +2 -0
- package/src/server-fns/admin-users/list.ts +2 -0
- package/src/server-fns/admin-users/set-password.ts +2 -0
- package/src/server-fns/admin-users/set-user-roles.ts +2 -0
- package/src/server-fns/admin-users/update.ts +2 -0
- package/src/server-fns/ai/execute.ts +3 -0
- package/src/server-fns/analytics/maintenance.ts +4 -0
- package/src/server-fns/analytics/queries.ts +14 -8
- package/src/server-fns/auth/current-user.ts +7 -3
- package/src/server-fns/auth/renew.test.node.ts +85 -0
- package/src/server-fns/auth/renew.ts +54 -0
- package/src/server-fns/auth/sign-in.test.node.ts +114 -0
- package/src/server-fns/auth/sign-in.ts +80 -27
- package/src/server-fns/auth/sign-out.test.node.ts +85 -0
- package/src/server-fns/auth/sign-out.ts +44 -19
- package/src/server-fns/collections/audit.ts +2 -0
- package/src/server-fns/collections/copy-to-locale.ts +3 -1
- package/src/server-fns/collections/create.ts +3 -1
- package/src/server-fns/collections/delete-locale.ts +3 -1
- package/src/server-fns/collections/delete.ts +3 -1
- package/src/server-fns/collections/duplicate.ts +3 -1
- package/src/server-fns/collections/get.ts +4 -1
- package/src/server-fns/collections/history.ts +2 -0
- package/src/server-fns/collections/list.ts +3 -1
- package/src/server-fns/collections/reindex.ts +2 -0
- package/src/server-fns/collections/reorder.ts +3 -1
- package/src/server-fns/collections/restore-version.ts +3 -1
- package/src/server-fns/collections/revision-input.test.node.ts +14 -0
- package/src/server-fns/collections/scheduled-publication.ts +14 -7
- package/src/server-fns/collections/set-list-view-preference.ts +2 -0
- package/src/server-fns/collections/stats.ts +2 -0
- package/src/server-fns/collections/status.ts +4 -1
- package/src/server-fns/collections/tree.ts +7 -1
- package/src/server-fns/collections/update.ts +4 -1
- package/src/server-fns/collections/upload-singleton.test.node.ts +8 -1
- package/src/server-fns/collections/upload.ts +10 -0
- package/src/server-fns/i18n/set-locale.ts +19 -3
- package/src/server-fns/preview/disable.ts +8 -4
- package/src/server-fns/preview/enable.ts +9 -5
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +3 -1
- package/src/server-fns/singletons/change-status.ts +3 -1
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +3 -1
- package/src/server-fns/singletons/copy-to-locale.ts +3 -1
- package/src/server-fns/singletons/find-by-version.ts +2 -0
- package/src/server-fns/singletons/get-scheduled-publish.ts +2 -0
- package/src/server-fns/singletons/get.ts +2 -0
- package/src/server-fns/singletons/history.ts +2 -0
- package/src/server-fns/singletons/restore-version.ts +3 -1
- package/src/server-fns/singletons/schedule-publish.ts +3 -1
- package/src/server-fns/singletons/singletons.test.node.ts +6 -0
- package/src/server-fns/singletons/unpublish.ts +3 -1
- package/src/server-fns/singletons/update.ts +3 -1
|
@@ -12,11 +12,13 @@ import { enableAdminUserCommand, type OkResponse } from '@byline/admin/admin-use
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
// No UI affordance yet — paired with `disable.ts`, both ship ahead of the
|
|
17
18
|
// detail-view enable/disable control they will be wired into. Keep until
|
|
18
19
|
// that control lands; not dead code.
|
|
19
20
|
export const enableAdminUser = createServerFn({ method: 'POST' })
|
|
21
|
+
.middleware([adminSessionMiddleware])
|
|
20
22
|
.validator((input: { id: string }) => input)
|
|
21
23
|
.handler(async ({ data }): Promise<OkResponse> => {
|
|
22
24
|
const context = await getAdminRequestContext()
|
|
@@ -12,8 +12,10 @@ import { getRolesForUserCommand, type UserRolesResponse } from '@byline/admin/ad
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
export const getUserRoles = createServerFn({ method: 'GET' })
|
|
18
|
+
.middleware([adminSessionMiddleware])
|
|
17
19
|
.validator((input: { userId: string }) => input)
|
|
18
20
|
.handler(async ({ data }): Promise<UserRolesResponse> => {
|
|
19
21
|
const context = await getAdminRequestContext()
|
|
@@ -12,8 +12,10 @@ import { type AdminUserResponse, getAdminUserCommand } from '@byline/admin/admin
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
export const getAdminUser = createServerFn({ method: 'GET' })
|
|
18
|
+
.middleware([adminSessionMiddleware])
|
|
17
19
|
.validator((input: { id: string }) => input)
|
|
18
20
|
.handler(async ({ data }): Promise<AdminUserResponse> => {
|
|
19
21
|
const context = await getAdminRequestContext()
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
17
17
|
|
|
18
18
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
19
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
19
20
|
|
|
20
21
|
export interface ListAdminUsersInput {
|
|
21
22
|
page?: number
|
|
@@ -33,6 +34,7 @@ export interface ListAdminUsersInput {
|
|
|
33
34
|
* TanStack Start transport layer for the client to branch on.
|
|
34
35
|
*/
|
|
35
36
|
export const listAdminUsers = createServerFn({ method: 'GET' })
|
|
37
|
+
.middleware([adminSessionMiddleware])
|
|
36
38
|
.validator((input: ListAdminUsersInput) => input ?? {})
|
|
37
39
|
.handler(async ({ data }): Promise<AdminUserListResponse> => {
|
|
38
40
|
const context = await getAdminRequestContext()
|
|
@@ -12,6 +12,7 @@ import { type AdminUserResponse, setAdminUserPasswordCommand } from '@byline/adm
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
export interface SetAdminUserPasswordInput {
|
|
17
18
|
id: string
|
|
@@ -20,6 +21,7 @@ export interface SetAdminUserPasswordInput {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export const setAdminUserPassword = createServerFn({ method: 'POST' })
|
|
24
|
+
.middleware([adminSessionMiddleware])
|
|
23
25
|
.validator((input: SetAdminUserPasswordInput) => input)
|
|
24
26
|
.handler(async ({ data }): Promise<AdminUserResponse> => {
|
|
25
27
|
const context = await getAdminRequestContext()
|
|
@@ -12,6 +12,7 @@ import { setRolesForUserCommand, type UserRolesResponse } from '@byline/admin/ad
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
export interface SetUserRolesInput {
|
|
17
18
|
userId: string
|
|
@@ -19,6 +20,7 @@ export interface SetUserRolesInput {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export const setUserRoles = createServerFn({ method: 'POST' })
|
|
23
|
+
.middleware([adminSessionMiddleware])
|
|
22
24
|
.validator((input: SetUserRolesInput) => input)
|
|
23
25
|
.handler(async ({ data }): Promise<UserRolesResponse> => {
|
|
24
26
|
const context = await getAdminRequestContext()
|
|
@@ -12,6 +12,7 @@ import { type AdminUserResponse, updateAdminUserCommand } from '@byline/admin/ad
|
|
|
12
12
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
13
13
|
|
|
14
14
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
15
16
|
|
|
16
17
|
export interface UpdateAdminUserInput {
|
|
17
18
|
id: string
|
|
@@ -27,6 +28,7 @@ export interface UpdateAdminUserInput {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export const updateAdminUser = createServerFn({ method: 'POST' })
|
|
31
|
+
.middleware([adminSessionMiddleware])
|
|
30
32
|
.validator((input: UpdateAdminUserInput) => input)
|
|
31
33
|
.handler(async ({ data }): Promise<AdminUserResponse> => {
|
|
32
34
|
const context = await getAdminRequestContext()
|
|
@@ -26,6 +26,8 @@ import type { ExecuteInstructionOptions, ExecuteInstructionParams } from '@bylin
|
|
|
26
26
|
import { executeInstruction, executeInstructionStreaming } from '@byline/ai/server'
|
|
27
27
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
28
28
|
|
|
29
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
30
|
+
|
|
29
31
|
/**
|
|
30
32
|
* Wire shape — the same as `ExecuteInstruction` minus the in-process-only
|
|
31
33
|
* `signal` (an AbortSignal can't be serialized across the RPC boundary;
|
|
@@ -37,6 +39,7 @@ type ExecuteAiInstructionInput = {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export const executeAiInstruction = createServerFn({ method: 'POST' })
|
|
42
|
+
.middleware([adminSessionMiddleware])
|
|
40
43
|
.validator((input: ExecuteAiInstructionInput) => input)
|
|
41
44
|
.handler(async ({ data }): Promise<Response> => {
|
|
42
45
|
// Throws ERR_UNAUTHENTICATED if there is no admin session.
|
|
@@ -12,6 +12,8 @@ import { assertAdminActor } from '@byline/admin'
|
|
|
12
12
|
import { ANALYTICS_ABILITIES } from '@byline/admin/analytics'
|
|
13
13
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
14
14
|
|
|
15
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
16
|
+
|
|
15
17
|
export interface DeleteAnalyticsEventsInput {
|
|
16
18
|
from: string
|
|
17
19
|
to: string
|
|
@@ -19,6 +21,7 @@ export interface DeleteAnalyticsEventsInput {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const deleteAnalyticsEvents = createServerFn({ method: 'POST' })
|
|
24
|
+
.middleware([adminSessionMiddleware])
|
|
22
25
|
.validator((input: DeleteAnalyticsEventsInput) => input)
|
|
23
26
|
.handler(async ({ data }): Promise<{ deleted: number }> => {
|
|
24
27
|
await assertAnalyticsMaintenance()
|
|
@@ -34,6 +37,7 @@ export const deleteAnalyticsEvents = createServerFn({ method: 'POST' })
|
|
|
34
37
|
})
|
|
35
38
|
|
|
36
39
|
export const rebuildAnalyticsDay = createServerFn({ method: 'POST' })
|
|
40
|
+
.middleware([adminSessionMiddleware])
|
|
37
41
|
.validator((input: { day: string }) => input)
|
|
38
42
|
.handler(async ({ data }): Promise<{ rebuilt: string }> => {
|
|
39
43
|
await assertAnalyticsMaintenance()
|
|
@@ -22,6 +22,8 @@ import type {
|
|
|
22
22
|
} from '@byline/analytics'
|
|
23
23
|
import { getAdminRequestContext } from '@byline/client/server'
|
|
24
24
|
|
|
25
|
+
import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
|
|
26
|
+
|
|
25
27
|
export interface AnalyticsTopInput extends AnalyticsDateRange {
|
|
26
28
|
kind: AnalyticsEventKind
|
|
27
29
|
limit?: number
|
|
@@ -31,16 +33,17 @@ export interface AnalyticsRuntimeState {
|
|
|
31
33
|
enabled: boolean
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
export const getAnalyticsRuntime = createServerFn({ method: 'GET' })
|
|
35
|
-
|
|
36
|
+
export const getAnalyticsRuntime = createServerFn({ method: 'GET' })
|
|
37
|
+
.middleware([adminSessionMiddleware])
|
|
38
|
+
.handler(async (): Promise<AnalyticsRuntimeState> => {
|
|
36
39
|
const context = await getAdminRequestContext()
|
|
37
40
|
requireAdminActor(context, 'reading analytics runtime state')
|
|
38
41
|
const { isAnalyticsRegistered } = await import('@byline/analytics')
|
|
39
42
|
return { enabled: isAnalyticsRegistered() }
|
|
40
|
-
}
|
|
41
|
-
)
|
|
43
|
+
})
|
|
42
44
|
|
|
43
45
|
export const getAnalyticsSummary = createServerFn({ method: 'GET' })
|
|
46
|
+
.middleware([adminSessionMiddleware])
|
|
44
47
|
.validator((input: AnalyticsDateRange) => input)
|
|
45
48
|
.handler(async ({ data }): Promise<AnalyticsSummary> => {
|
|
46
49
|
await assertAnalyticsRead()
|
|
@@ -48,15 +51,16 @@ export const getAnalyticsSummary = createServerFn({ method: 'GET' })
|
|
|
48
51
|
return getAnalytics().getSummary(data)
|
|
49
52
|
})
|
|
50
53
|
|
|
51
|
-
export const getAnalyticsReportCoverage = createServerFn({ method: 'GET' })
|
|
52
|
-
|
|
54
|
+
export const getAnalyticsReportCoverage = createServerFn({ method: 'GET' })
|
|
55
|
+
.middleware([adminSessionMiddleware])
|
|
56
|
+
.handler(async (): Promise<AnalyticsReportCoverage> => {
|
|
53
57
|
await assertAnalyticsRead()
|
|
54
58
|
const { getAnalytics } = await import('@byline/analytics')
|
|
55
59
|
return getAnalytics().getReportCoverage()
|
|
56
|
-
}
|
|
57
|
-
)
|
|
60
|
+
})
|
|
58
61
|
|
|
59
62
|
export const getAnalyticsTop = createServerFn({ method: 'GET' })
|
|
63
|
+
.middleware([adminSessionMiddleware])
|
|
60
64
|
.validator((input: AnalyticsTopInput) => input)
|
|
61
65
|
.handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsPathTotal>> => {
|
|
62
66
|
await assertAnalyticsRead()
|
|
@@ -65,6 +69,7 @@ export const getAnalyticsTop = createServerFn({ method: 'GET' })
|
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
export const getAnalyticsReferrers = createServerFn({ method: 'GET' })
|
|
72
|
+
.middleware([adminSessionMiddleware])
|
|
68
73
|
.validator((input: AnalyticsDateRange & { limit?: number }) => input)
|
|
69
74
|
.handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsReferrerTotal>> => {
|
|
70
75
|
await assertAnalyticsRead()
|
|
@@ -73,6 +78,7 @@ export const getAnalyticsReferrers = createServerFn({ method: 'GET' })
|
|
|
73
78
|
})
|
|
74
79
|
|
|
75
80
|
export const getAnalyticsCountries = createServerFn({ method: 'GET' })
|
|
81
|
+
.middleware([adminSessionMiddleware])
|
|
76
82
|
.validator((input: AnalyticsDateRange) => input)
|
|
77
83
|
.handler(async ({ data }): Promise<AnalyticsCountryTotal[]> => {
|
|
78
84
|
await assertAnalyticsRead()
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Admin profile lookup for the authenticated request.
|
|
11
11
|
*
|
|
12
12
|
* Thin wrapper that resolves the `AdminAuth` via `getAdminRequestContext()`
|
|
13
|
-
* (which
|
|
13
|
+
* (which verifies credentials without rotating or rewriting cookies), then fetches the
|
|
14
14
|
* admin user's profile row so the UI can render "signed in as X" without
|
|
15
15
|
* embedding identity metadata in the JWT itself.
|
|
16
16
|
*
|
|
@@ -26,6 +26,7 @@ import { getAdminRequestContext } from '@byline/client/server'
|
|
|
26
26
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
27
27
|
|
|
28
28
|
export interface CurrentAdminUser {
|
|
29
|
+
sessionId: string
|
|
29
30
|
id: string
|
|
30
31
|
email: string
|
|
31
32
|
given_name: string | null
|
|
@@ -48,7 +49,7 @@ export interface CurrentAdminUser {
|
|
|
48
49
|
|
|
49
50
|
export const getCurrentAdminUser = createServerFn({ method: 'GET' }).handler(
|
|
50
51
|
async (): Promise<CurrentAdminUser> => {
|
|
51
|
-
const { actor } = await getAdminRequestContext()
|
|
52
|
+
const { actor, sessionId } = await getAdminRequestContext()
|
|
52
53
|
if (!actor) {
|
|
53
54
|
// getAdminRequestContext always returns an AdminAuth on success; the
|
|
54
55
|
// null branch is compile-time impossible here but satisfies the type.
|
|
@@ -63,6 +64,7 @@ export const getCurrentAdminUser = createServerFn({ method: 'GET' }).handler(
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
return {
|
|
67
|
+
sessionId: sessionId!,
|
|
66
68
|
id: row.id,
|
|
67
69
|
email: row.email,
|
|
68
70
|
given_name: row.given_name,
|
|
@@ -85,8 +87,9 @@ export const getCurrentAdminUserSoft = createServerFn({ method: 'GET' }).handler
|
|
|
85
87
|
async (): Promise<CurrentAdminUser | null> => {
|
|
86
88
|
try {
|
|
87
89
|
let actor: Actor
|
|
90
|
+
let sessionId: string | undefined
|
|
88
91
|
try {
|
|
89
|
-
;({ actor } = await getAdminRequestContext())
|
|
92
|
+
;({ actor, sessionId } = await getAdminRequestContext())
|
|
90
93
|
} catch (err) {
|
|
91
94
|
if (err instanceof AuthError) return null
|
|
92
95
|
throw err
|
|
@@ -97,6 +100,7 @@ export const getCurrentAdminUserSoft = createServerFn({ method: 'GET' }).handler
|
|
|
97
100
|
if (!row) return null
|
|
98
101
|
|
|
99
102
|
return {
|
|
103
|
+
sessionId: sessionId!,
|
|
100
104
|
id: row.id,
|
|
101
105
|
email: row.email,
|
|
102
106
|
given_name: row.given_name,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ERR_ACCESS_EXPIRED, ERR_INVALID_TOKEN, ERR_REVOKED_TOKEN } from '@byline/auth'
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
|
|
12
|
+
const mocks = vi.hoisted(() => ({
|
|
13
|
+
access: 'access' as string | undefined,
|
|
14
|
+
refresh: 'refresh' as string | undefined,
|
|
15
|
+
verify: vi.fn(),
|
|
16
|
+
rotate: vi.fn(),
|
|
17
|
+
cookies: vi.fn(),
|
|
18
|
+
}))
|
|
19
|
+
vi.mock('@tanstack/react-start', () => ({
|
|
20
|
+
createServerFn: () => ({
|
|
21
|
+
validator: (validate: any) => ({ handler: (handler: any) => ({ validate, handler }) }),
|
|
22
|
+
}),
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('@tanstack/react-start/server', () => ({ setResponseHeader: vi.fn() }))
|
|
25
|
+
vi.mock('@byline/core', () => ({
|
|
26
|
+
getServerConfig: () => ({
|
|
27
|
+
sessionProvider: { verifyAccessToken: mocks.verify, refreshSession: mocks.rotate },
|
|
28
|
+
}),
|
|
29
|
+
}))
|
|
30
|
+
vi.mock('@byline/client/server', () => ({
|
|
31
|
+
readAccessTokenCookie: () => mocks.access,
|
|
32
|
+
readRefreshTokenCookie: () => mocks.refresh,
|
|
33
|
+
setSessionCookies: mocks.cookies,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
import { renewAdminSession } from './renew.js'
|
|
37
|
+
|
|
38
|
+
const endpoint = renewAdminSession as unknown as { handler: (args: any) => Promise<any> }
|
|
39
|
+
const call = () => endpoint.handler({ data: { expectedSessionId: 'A' } })
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.resetAllMocks()
|
|
42
|
+
mocks.access = 'access'
|
|
43
|
+
mocks.refresh = 'refresh'
|
|
44
|
+
})
|
|
45
|
+
describe('explicit renewal', () => {
|
|
46
|
+
it('does not rotate or write cookies when access is already valid', async () => {
|
|
47
|
+
mocks.verify.mockResolvedValue({ sessionId: 'A' })
|
|
48
|
+
await expect(call()).resolves.toEqual({ sessionId: 'A' })
|
|
49
|
+
expect(mocks.rotate).not.toHaveBeenCalled()
|
|
50
|
+
expect(mocks.cookies).not.toHaveBeenCalled()
|
|
51
|
+
})
|
|
52
|
+
it('passes the original login identity into renewal and writes only confirmed credentials', async () => {
|
|
53
|
+
mocks.verify.mockRejectedValue(ERR_ACCESS_EXPIRED({ message: 'expired' }))
|
|
54
|
+
const tokens = { sessionId: 'A', accessToken: 'new', refreshToken: 'new-refresh' }
|
|
55
|
+
mocks.rotate.mockImplementation(async () => {
|
|
56
|
+
expect(mocks.cookies).not.toHaveBeenCalled()
|
|
57
|
+
return tokens
|
|
58
|
+
})
|
|
59
|
+
await expect(call()).resolves.toEqual({ sessionId: 'A' })
|
|
60
|
+
expect(mocks.rotate).toHaveBeenCalledWith({ refreshToken: 'refresh', expectedSessionId: 'A' })
|
|
61
|
+
expect(mocks.cookies).toHaveBeenCalledWith(tokens)
|
|
62
|
+
})
|
|
63
|
+
it.each([
|
|
64
|
+
ERR_INVALID_TOKEN({ message: 'bad signature' }),
|
|
65
|
+
ERR_REVOKED_TOKEN({ message: 'revoked' }),
|
|
66
|
+
new Error('database'),
|
|
67
|
+
])('does not turn %s into renewal', async (error) => {
|
|
68
|
+
mocks.verify.mockRejectedValue(error)
|
|
69
|
+
await expect(call()).rejects.toBe(error)
|
|
70
|
+
expect(mocks.rotate).not.toHaveBeenCalled()
|
|
71
|
+
expect(mocks.cookies).not.toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
it('rejects a different live login instead of accepting the no-op', async () => {
|
|
74
|
+
mocks.verify.mockResolvedValue({ sessionId: 'B' })
|
|
75
|
+
await expect(call()).rejects.toMatchObject({ code: 'ERR_SESSION_CHANGED' })
|
|
76
|
+
expect(mocks.rotate).not.toHaveBeenCalled()
|
|
77
|
+
expect(mocks.cookies).not.toHaveBeenCalled()
|
|
78
|
+
})
|
|
79
|
+
it('preserves the cookie jar on renewal failure', async () => {
|
|
80
|
+
mocks.access = undefined
|
|
81
|
+
mocks.rotate.mockRejectedValue(ERR_REVOKED_TOKEN({ message: 'replayed' }))
|
|
82
|
+
await expect(call()).rejects.toThrow()
|
|
83
|
+
expect(mocks.cookies).not.toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
import { setResponseHeader } from '@tanstack/react-start/server'
|
|
11
|
+
|
|
12
|
+
import { AuthError, ERR_SESSION_CHANGED, ERR_UNAUTHENTICATED } from '@byline/auth'
|
|
13
|
+
import {
|
|
14
|
+
readAccessTokenCookie,
|
|
15
|
+
readRefreshTokenCookie,
|
|
16
|
+
setSessionCookies,
|
|
17
|
+
} from '@byline/client/server'
|
|
18
|
+
import { getServerConfig } from '@byline/core'
|
|
19
|
+
|
|
20
|
+
/** Explicit renewal only. Global CSRF protection applies before this POST handler. */
|
|
21
|
+
export const renewAdminSession = createServerFn({ method: 'POST' })
|
|
22
|
+
.validator((value: unknown): { expectedSessionId?: string } => {
|
|
23
|
+
if (value == null) return {}
|
|
24
|
+
if (typeof value !== 'object') throw new Error('Invalid renewal input')
|
|
25
|
+
const sid = (value as { expectedSessionId?: unknown }).expectedSessionId
|
|
26
|
+
if (sid !== undefined && (typeof sid !== 'string' || !sid || sid.length > 256))
|
|
27
|
+
throw new Error('Invalid login identity')
|
|
28
|
+
return { expectedSessionId: sid as string | undefined }
|
|
29
|
+
})
|
|
30
|
+
.handler(async ({ data }) => {
|
|
31
|
+
setResponseHeader('Cache-Control', 'no-store')
|
|
32
|
+
const provider = getServerConfig().sessionProvider
|
|
33
|
+
if (!provider) throw new Error('no sessionProvider configured')
|
|
34
|
+
const access = readAccessTokenCookie()
|
|
35
|
+
if (access) {
|
|
36
|
+
try {
|
|
37
|
+
const verified = await provider.verifyAccessToken(access)
|
|
38
|
+
if (data.expectedSessionId && data.expectedSessionId !== verified.sessionId)
|
|
39
|
+
throw ERR_SESSION_CHANGED({ message: 'session changed' })
|
|
40
|
+
return { sessionId: verified.sessionId }
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Invalid signature, revocation and infrastructure errors never cause renewal.
|
|
43
|
+
if (!(error instanceof AuthError) || error.code !== 'ERR_ACCESS_EXPIRED') throw error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const refreshToken = readRefreshTokenCookie()
|
|
47
|
+
if (!refreshToken) throw ERR_UNAUTHENTICATED({ message: 'no session to renew' })
|
|
48
|
+
const tokens = await provider.refreshSession({
|
|
49
|
+
refreshToken,
|
|
50
|
+
expectedSessionId: data.expectedSessionId,
|
|
51
|
+
})
|
|
52
|
+
setSessionCookies(tokens)
|
|
53
|
+
return { sessionId: tokens.sessionId }
|
|
54
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
|
|
11
|
+
const mocks = vi.hoisted(() => ({
|
|
12
|
+
config: {} as any,
|
|
13
|
+
request: null as unknown as Request,
|
|
14
|
+
signIn: vi.fn(),
|
|
15
|
+
limiter: vi.fn(),
|
|
16
|
+
acquire: vi.fn(),
|
|
17
|
+
release: vi.fn(),
|
|
18
|
+
outcome: vi.fn(),
|
|
19
|
+
setCookies: vi.fn(),
|
|
20
|
+
}))
|
|
21
|
+
vi.mock('@tanstack/react-start', () => ({
|
|
22
|
+
createServerFn: () => ({
|
|
23
|
+
validator: (validate: (input: unknown) => unknown) => ({
|
|
24
|
+
handler: (handler: any) => ({ validate, handler }),
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
}))
|
|
28
|
+
vi.mock('@tanstack/react-start/server', () => ({
|
|
29
|
+
getRequest: () => mocks.request,
|
|
30
|
+
getRequestHeader: () => undefined,
|
|
31
|
+
}))
|
|
32
|
+
vi.mock('@byline/core', () => ({ getServerConfig: () => mocks.config }))
|
|
33
|
+
vi.mock('@byline/client/server', () => ({
|
|
34
|
+
setSessionCookies: mocks.setCookies,
|
|
35
|
+
readAccessTokenCookie: () => undefined,
|
|
36
|
+
readRefreshTokenCookie: () => undefined,
|
|
37
|
+
}))
|
|
38
|
+
vi.mock('../../i18n/locale-cookie.js', () => ({ readAdminLocaleCookie: () => null }))
|
|
39
|
+
vi.mock('../../integrations/byline-core.js', () => ({
|
|
40
|
+
bylineCore: () => {
|
|
41
|
+
throw new Error('unused')
|
|
42
|
+
},
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
import { checkSignInBody } from '../../integrations/sign-in-body.js'
|
|
46
|
+
import { adminSignIn } from './sign-in.js'
|
|
47
|
+
|
|
48
|
+
const fn = adminSignIn as unknown as {
|
|
49
|
+
validate: (input: unknown) => unknown
|
|
50
|
+
handler: (args: any) => Promise<unknown>
|
|
51
|
+
}
|
|
52
|
+
const input = { email: 'admin@example.test', password: 'legacy' }
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
vi.clearAllMocks()
|
|
55
|
+
mocks.request = new Request('http://localhost/sign-in', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
body: '{}',
|
|
59
|
+
})
|
|
60
|
+
await checkSignInBody(mocks.request)
|
|
61
|
+
mocks.acquire.mockResolvedValue(mocks.release)
|
|
62
|
+
mocks.limiter.mockResolvedValue({ allowed: true, retryAfterSeconds: 0 })
|
|
63
|
+
mocks.signIn.mockResolvedValue({ actor: { id: 'test' } })
|
|
64
|
+
mocks.config = {
|
|
65
|
+
sessionProvider: { signInWithPassword: mocks.signIn },
|
|
66
|
+
passwordSignIn: {
|
|
67
|
+
resolveClientIp: () => '192.0.2.1',
|
|
68
|
+
limiter: { consume: mocks.limiter, acquire: mocks.acquire, recordResult: mocks.outcome },
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
describe('password sign-in protection', () => {
|
|
73
|
+
it('admits validated legacy credentials and sets cookies after authentication', async () => {
|
|
74
|
+
expect(await fn.handler({ data: fn.validate(input) })).toEqual({ userId: 'test' })
|
|
75
|
+
expect(mocks.limiter).toHaveBeenCalledWith({ email: input.email, ip: '192.0.2.1' })
|
|
76
|
+
expect(mocks.signIn).toHaveBeenCalledWith({ ...input, ip: '192.0.2.1', userAgent: undefined })
|
|
77
|
+
expect(mocks.setCookies).toHaveBeenCalledTimes(1)
|
|
78
|
+
})
|
|
79
|
+
it('rejects oversized credentials during validation', () => {
|
|
80
|
+
expect(() => fn.validate({ ...input, password: 'a'.repeat(129) })).toThrow()
|
|
81
|
+
expect(mocks.signIn).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
it('returns 429 with Retry-After before the provider is called', async () => {
|
|
84
|
+
mocks.limiter.mockResolvedValue({ allowed: false, retryAfterSeconds: 42 })
|
|
85
|
+
const error = await fn.handler({ data: input }).catch((e) => e)
|
|
86
|
+
expect(error.status).toBe(429)
|
|
87
|
+
expect(error.headers.get('Retry-After')).toBe('42')
|
|
88
|
+
expect(mocks.signIn).not.toHaveBeenCalled()
|
|
89
|
+
expect(mocks.setCookies).not.toHaveBeenCalled()
|
|
90
|
+
})
|
|
91
|
+
it('sheds capacity before accessing the store or provider', async () => {
|
|
92
|
+
mocks.acquire.mockResolvedValue(null)
|
|
93
|
+
expect((await fn.handler({ data: input }).catch((e) => e)).status).toBe(429)
|
|
94
|
+
expect(mocks.limiter).not.toHaveBeenCalled()
|
|
95
|
+
expect(mocks.signIn).not.toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
it('releases capacity and records failed verification', async () => {
|
|
98
|
+
mocks.signIn.mockRejectedValueOnce(new Error('invalid credentials'))
|
|
99
|
+
await expect(fn.handler({ data: input })).rejects.toThrow('invalid credentials')
|
|
100
|
+
expect(mocks.release).toHaveBeenCalledTimes(1)
|
|
101
|
+
expect(mocks.outcome).toHaveBeenCalledWith({ email: input.email, ip: '192.0.2.1' }, 'failure')
|
|
102
|
+
})
|
|
103
|
+
it.each(['store', 'ip', 'middleware', 'config'])(
|
|
104
|
+
'fails closed when %s protection is unavailable',
|
|
105
|
+
async (kind) => {
|
|
106
|
+
if (kind === 'store') mocks.limiter.mockRejectedValue(new Error('DB unavailable'))
|
|
107
|
+
if (kind === 'ip') mocks.config.passwordSignIn.resolveClientIp = () => null
|
|
108
|
+
if (kind === 'middleware') mocks.request = new Request('http://localhost/sign-in')
|
|
109
|
+
if (kind === 'config') delete mocks.config.passwordSignIn
|
|
110
|
+
expect((await fn.handler({ data: input }).catch((e) => e)).status).toBe(503)
|
|
111
|
+
expect(mocks.signIn).not.toHaveBeenCalled()
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
})
|
|
@@ -18,13 +18,21 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { createServerFn } from '@tanstack/react-start'
|
|
21
|
-
import { getRequestHeader } from '@tanstack/react-start/server'
|
|
22
|
-
|
|
23
|
-
import {
|
|
21
|
+
import { getRequest, getRequestHeader } from '@tanstack/react-start/server'
|
|
22
|
+
|
|
23
|
+
import type { SignInAdmission } from '@byline/auth'
|
|
24
|
+
import {
|
|
25
|
+
readAccessTokenCookie,
|
|
26
|
+
readRefreshTokenCookie,
|
|
27
|
+
setSessionCookies,
|
|
28
|
+
} from '@byline/client/server'
|
|
24
29
|
import { getServerConfig } from '@byline/core'
|
|
30
|
+
import { passwordSignInSchema } from '@byline/core/validation'
|
|
25
31
|
|
|
26
32
|
import { readAdminLocaleCookie } from '../../i18n/locale-cookie.js'
|
|
27
33
|
import { bylineCore } from '../../integrations/byline-core.js'
|
|
34
|
+
import { normalizeClientIp } from '../../integrations/client-ip.js'
|
|
35
|
+
import { wasSignInBodyChecked } from '../../integrations/sign-in-body.js'
|
|
28
36
|
|
|
29
37
|
export interface SignInInput {
|
|
30
38
|
email: string
|
|
@@ -33,17 +41,14 @@ export interface SignInInput {
|
|
|
33
41
|
|
|
34
42
|
export interface SignInResult {
|
|
35
43
|
userId: string
|
|
44
|
+
sessionId: string
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export const adminSignIn = createServerFn({ method: 'POST' })
|
|
39
|
-
.validator((input:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (typeof input?.password !== 'string' || input.password.length === 0) {
|
|
44
|
-
throw new Error('password is required')
|
|
45
|
-
}
|
|
46
|
-
return { email: input.email, password: input.password }
|
|
48
|
+
.validator((input: unknown) => {
|
|
49
|
+
const parsed = passwordSignInSchema.safeParse(input)
|
|
50
|
+
if (!parsed.success) throw new Response('Invalid sign-in input', { status: 400 })
|
|
51
|
+
return parsed.data
|
|
47
52
|
})
|
|
48
53
|
.handler(async ({ data }): Promise<SignInResult> => {
|
|
49
54
|
const provider = getServerConfig().sessionProvider
|
|
@@ -51,21 +56,69 @@ export const adminSignIn = createServerFn({ method: 'POST' })
|
|
|
51
56
|
throw new Error('no sessionProvider configured')
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
59
|
+
const request = getRequest()
|
|
60
|
+
const protection = getServerConfig().passwordSignIn
|
|
61
|
+
if (!wasSignInBodyChecked(request)) {
|
|
62
|
+
throw new Response('Password sign-in request middleware is not configured', { status: 503 })
|
|
63
|
+
}
|
|
64
|
+
if (!protection) {
|
|
65
|
+
throw new Response('Password sign-in protection is not configured', { status: 503 })
|
|
66
|
+
}
|
|
67
|
+
const userAgent = getRequestHeader('user-agent')?.slice(0, 512)
|
|
68
|
+
let ip: string | null
|
|
69
|
+
try {
|
|
70
|
+
ip = normalizeClientIp(await protection.resolveClientIp(request))
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Response('Client address unavailable', { status: 503 })
|
|
73
|
+
}
|
|
74
|
+
if (!ip) throw new Response('Client address unavailable', { status: 503 })
|
|
75
|
+
const tooMany = (seconds: number) =>
|
|
76
|
+
new Response('Too many sign-in attempts', {
|
|
77
|
+
status: 429,
|
|
78
|
+
headers: { 'Retry-After': String(seconds), 'Cache-Control': 'no-store' },
|
|
79
|
+
})
|
|
80
|
+
let release: (() => void) | null
|
|
81
|
+
try {
|
|
82
|
+
release = await protection.limiter.acquire()
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Response('Sign-in temporarily unavailable', { status: 503 })
|
|
85
|
+
}
|
|
86
|
+
if (!release) throw tooMany(1)
|
|
87
|
+
const recordResult = (outcome: 'success' | 'failure') => {
|
|
88
|
+
try {
|
|
89
|
+
protection.limiter.recordResult?.({ email: data.email, ip }, outcome)
|
|
90
|
+
} catch {
|
|
91
|
+
/* Telemetry cannot change authentication outcomes. */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const result = await (async () => {
|
|
95
|
+
try {
|
|
96
|
+
let admission: SignInAdmission
|
|
97
|
+
try {
|
|
98
|
+
admission = await protection.limiter.consume({ email: data.email, ip })
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Response('Sign-in temporarily unavailable', { status: 503 })
|
|
101
|
+
}
|
|
102
|
+
if (!admission.allowed) throw tooMany(admission.retryAfterSeconds)
|
|
103
|
+
try {
|
|
104
|
+
const result = await provider.signInWithPassword({
|
|
105
|
+
previousAccessToken: readAccessTokenCookie(),
|
|
106
|
+
previousRefreshToken: readRefreshTokenCookie(),
|
|
107
|
+
email: data.email,
|
|
108
|
+
password: data.password,
|
|
109
|
+
userAgent,
|
|
110
|
+
ip,
|
|
111
|
+
})
|
|
112
|
+
recordResult('success')
|
|
113
|
+
return result
|
|
114
|
+
} catch (error) {
|
|
115
|
+
recordResult('failure')
|
|
116
|
+
throw error
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
release()
|
|
120
|
+
}
|
|
121
|
+
})()
|
|
69
122
|
|
|
70
123
|
setSessionCookies(result)
|
|
71
124
|
|
|
@@ -82,7 +135,7 @@ export const adminSignIn = createServerFn({ method: 'POST' })
|
|
|
82
135
|
// Swallow — locale sync is not load-bearing for the sign-in flow.
|
|
83
136
|
}
|
|
84
137
|
|
|
85
|
-
return { userId: result.actor.id }
|
|
138
|
+
return { userId: result.actor.id, sessionId: result.sessionId }
|
|
86
139
|
})
|
|
87
140
|
|
|
88
141
|
/**
|