@cytario/web 2.1.4 → 2.1.5
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/README.md +20 -20
- package/app/.server/auth/README.md +8 -8
- package/app/.server/auth/authMiddleware.ts +9 -25
- package/app/.server/auth/exchangeAuthCode.ts +2 -6
- package/app/.server/auth/getS3Client.ts +3 -13
- package/app/.server/auth/getSessionCredentials.ts +6 -20
- package/app/.server/auth/getUserInfo.ts +2 -6
- package/app/.server/auth/keycloakAdmin/client.ts +2 -9
- package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
- package/app/.server/auth/keycloakAdmin/users.ts +7 -23
- package/app/.server/auth/oauthState.ts +4 -13
- package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
- package/app/.server/auth/refreshAuthTokens.ts +5 -19
- package/app/.server/auth/sessionMiddleware.ts +1 -4
- package/app/.server/auth/sessionStorage.ts +1 -4
- package/app/.server/auth/verifyIdToken.ts +1 -3
- package/app/.server/auth/wellKnownEndpoints.ts +1 -4
- package/app/.server/db/redis.ts +5 -1
- package/app/.server/logging.ts +1 -4
- package/app/.server/requestDurationMiddleware.ts +1 -4
- package/app/components/.client/ImageViewer/README.md +5 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
- package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
- package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
- package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
- package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
- package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
- package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
- package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
- package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
- package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
- package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
- package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
- package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
- package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
- package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
- package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
- package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
- package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
- package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
- package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
- package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
- package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
- package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
- package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
- package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
- package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
- package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
- package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
- package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
- package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
- package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
- package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
- package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
- package/app/components/AppHeader.tsx +1 -4
- package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
- package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
- package/app/components/ClientOnly.tsx +1 -1
- package/app/components/Container.tsx +3 -15
- package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
- package/app/components/DataGrid/DataGrid.tsx +7 -27
- package/app/components/DataGrid/WktSvg.tsx +2 -4
- package/app/components/DataGrid/getParquetSchema.ts +1 -4
- package/app/components/DescriptionList.tsx +1 -3
- package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
- package/app/components/DirectoryView/DirectoryView.tsx +10 -46
- package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
- package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
- package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
- package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
- package/app/components/DirectoryView/FilterBar.tsx +9 -48
- package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
- package/app/components/DirectoryView/filterNodes.ts +4 -11
- package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
- package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
- package/app/components/DirectoryView/useLayoutStore.ts +5 -25
- package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
- package/app/components/GlobalSearch/SearchBar.tsx +1 -7
- package/app/components/GlobalSearch/Suggestions.tsx +0 -1
- package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
- package/app/components/LavaLoader.tsx +4 -11
- package/app/components/Layout/Footer.tsx +1 -4
- package/app/components/Pills/ScopePill.tsx +2 -8
- package/app/components/Table/ColumnFilterInput.tsx +5 -20
- package/app/components/Table/ColumnResizeHandle.tsx +1 -5
- package/app/components/Table/ColumnSortButton.tsx +1 -5
- package/app/components/Table/SelectionFooter.tsx +3 -5
- package/app/components/Table/Table.tsx +5 -21
- package/app/components/Table/TableBodyRow.tsx +19 -31
- package/app/components/Table/TableHeaderRow.tsx +7 -28
- package/app/components/Table/TableMenu.tsx +4 -20
- package/app/components/Table/state/createTableStore.ts +3 -9
- package/app/components/Table/state/useTableStore.ts +1 -3
- package/app/components/Table/types.ts +4 -10
- package/app/components/Table/useColumnFilters.ts +1 -4
- package/app/components/Table/useColumnVisibility.ts +4 -11
- package/app/components/Table/useColumnWidths.ts +1 -3
- package/app/components/Table/useTableSorting.ts +4 -12
- package/app/components/Tooltip/Tooltip.tsx +4 -17
- package/app/components/Tooltip/TooltipSpan.tsx +5 -28
- package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
- package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
- package/app/components/Tooltip/useOverflowDetection.ts +2 -5
- package/app/components/UserMenu.tsx +2 -9
- package/app/entry.server.tsx +9 -19
- package/app/hooks/useSearchParam.ts +2 -4
- package/app/lib/bootstrapPluginsCore.ts +4 -9
- package/app/root.tsx +4 -15
- package/app/routes/admin/assertAdminScope.ts +1 -3
- package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
- package/app/routes/admin/assertGroupsInScope.ts +3 -11
- package/app/routes/admin/assertUsersInScope.ts +2 -8
- package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
- package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
- package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
- package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
- package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
- package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
- package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
- package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
- package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
- package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
- package/app/routes/admin/users/BulkActions.tsx +15 -38
- package/app/routes/admin/users/bulkUsers.action.ts +2 -9
- package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
- package/app/routes/admin/users/users.route.tsx +14 -63
- package/app/routes/api/cyberduck-profile.$name.ts +6 -2
- package/app/routes/auth/callback.route.tsx +8 -33
- package/app/routes/auth/login.route.tsx +8 -11
- package/app/routes/auth/logout.route.tsx +4 -14
- package/app/routes/config.route.tsx +1 -5
- package/app/routes/connections/connection.form.tsx +5 -14
- package/app/routes/connections/connection.schema.ts +4 -16
- package/app/routes/connections/connections.loader.ts +11 -23
- package/app/routes/connections/connections.route.tsx +1 -5
- package/app/routes/connections/connections.server.ts +1 -3
- package/app/routes/connections/createConnection.action.ts +2 -8
- package/app/routes/connections/createConnection.modal.tsx +3 -13
- package/app/routes/connections/deleteConnection.action.ts +1 -4
- package/app/routes/connections/updateConnection.action.ts +2 -8
- package/app/routes/connections/updateConnection.modal.tsx +4 -14
- package/app/routes/home/home.route.tsx +8 -33
- package/app/routes/layouts/ModalOutlet.tsx +6 -18
- package/app/routes/objects/objects.loader.ts +5 -19
- package/app/routes/objects/objects.route.tsx +11 -30
- package/app/routes/presign.route.tsx +5 -18
- package/app/routes/recent.route.tsx +1 -4
- package/app/routes/search.route.tsx +4 -17
- package/app/routes.ts +1 -4
- package/app/tailwind.css +17 -12
- package/app/types/cornerstone-codecs.d.ts +25 -29
- package/app/utils/connectionsStore/selectors.ts +2 -6
- package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
- package/app/utils/db/convertCsvToParquet.ts +1 -3
- package/app/utils/db/createDatabase.ts +1 -3
- package/app/utils/db/createSingleton.ts +1 -1
- package/app/utils/db/getBlobFromObjectNode.ts +3 -9
- package/app/utils/db/getGeomQuery.ts +1 -1
- package/app/utils/db/getMarkerInfoWasm.ts +1 -3
- package/app/utils/db/getTileBoundingBox.ts +1 -4
- package/app/utils/db/sqlQueries.ts +1 -4
- package/app/utils/fileType.ts +80 -10
- package/app/utils/filterObjects.ts +2 -5
- package/app/utils/localFilesStore/useFileStore.ts +7 -7
- package/app/utils/recentlyViewed.server.ts +1 -4
- package/app/utils/resourceId.ts +3 -11
- package/app/utils/s3Provider.ts +3 -7
- package/app/utils/signedFetch.ts +4 -13
- package/bin-src/codegen.ts +4 -1
- package/package.json +5 -1
- package/prisma/seed.ts +1 -2
- package/public/favicon/site.webmanifest +1 -1
- package/server.js +1 -4
- package/server.js.map +1 -1
- package/vite-plugins/cytario-plugins.ts +2 -8
package/README.md
CHANGED
|
@@ -15,16 +15,16 @@ For the hosted product, see [cytario.com](https://www.cytario.com).
|
|
|
15
15
|
|
|
16
16
|
## Architecture
|
|
17
17
|
|
|
18
|
-
| Layer
|
|
19
|
-
|
|
20
|
-
| Framework
|
|
21
|
-
| Language
|
|
22
|
-
| Visualization | deck.gl, Viv, DuckDB-WASM, Apache Arrow
|
|
23
|
-
| Styling
|
|
24
|
-
| Auth
|
|
25
|
-
| Database
|
|
26
|
-
| Cloud
|
|
27
|
-
| CI/CD
|
|
18
|
+
| Layer | Technology |
|
|
19
|
+
| ------------- | -------------------------------------------------------------------------- |
|
|
20
|
+
| Framework | React Router v7 (SSR), React 19, Vite 6 |
|
|
21
|
+
| Language | TypeScript (strict mode) |
|
|
22
|
+
| Visualization | deck.gl, Viv, DuckDB-WASM, Apache Arrow |
|
|
23
|
+
| Styling | Tailwind CSS, [@cytario/design](https://github.com/cytario/cytario-design) |
|
|
24
|
+
| Auth | OAuth 2.0 via Keycloak, STS for S3 credentials |
|
|
25
|
+
| Database | PostgreSQL (Prisma ORM), Redis/Valkey (sessions) |
|
|
26
|
+
| Cloud | AWS SDK v3 (S3, STS), presigned URLs |
|
|
27
|
+
| CI/CD | GitHub Actions, semantic-release, GHCR |
|
|
28
28
|
|
|
29
29
|
## Plugin model
|
|
30
30
|
|
|
@@ -133,11 +133,11 @@ package's default export must satisfy the
|
|
|
133
133
|
|
|
134
134
|
### CLI
|
|
135
135
|
|
|
136
|
-
| Command
|
|
137
|
-
|
|
136
|
+
| Command | Behaviour |
|
|
137
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
138
138
|
| `cytario-web build` | Codegen (Vite plugin reads `CYTARIO_PLUGINS`) + `react-router build` against the installed package root. |
|
|
139
|
-
| `cytario-web dev`
|
|
140
|
-
| `cytario-web start` | `NODE_ENV=production node server.ts` against the bundled `build/server/index.js`.
|
|
139
|
+
| `cytario-web dev` | Codegen + `react-router dev`. Extra args (`--port`, `--host`, …) are forwarded. |
|
|
140
|
+
| `cytario-web start` | `NODE_ENV=production node server.ts` against the bundled `build/server/index.js`. |
|
|
141
141
|
|
|
142
142
|
All subcommands operate against `@cytario/web`'s own install directory;
|
|
143
143
|
the consumer never needs to know the on-disk layout.
|
|
@@ -213,12 +213,12 @@ cd devenv
|
|
|
213
213
|
podman kube play local-deployment.yaml
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
| Service
|
|
217
|
-
|
|
218
|
-
| Keycloak
|
|
219
|
-
| MinIO
|
|
220
|
-
| PostgreSQL | 5433
|
|
221
|
-
| Valkey
|
|
216
|
+
| Service | Port | Description |
|
|
217
|
+
| ---------- | ---------- | -------------------------------- |
|
|
218
|
+
| Keycloak | 8080 | Identity provider (admin/admin) |
|
|
219
|
+
| MinIO | 9000, 9001 | S3-compatible object storage |
|
|
220
|
+
| PostgreSQL | 5433 | Application database |
|
|
221
|
+
| Valkey | 6379 | Session cache (Redis-compatible) |
|
|
222
222
|
|
|
223
223
|
To stop: `podman kube down devenv/local-deployment.yaml`
|
|
224
224
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
OAuth 2.0 Authorization Code Flow against Keycloak. Session cookies are httpOnly, secure, SameSite=Lax; session data lives in Redis/Valkey. STS `AssumeRoleWithWebIdentity` mints per-connection S3 credentials using the user's Keycloak idToken.
|
|
4
4
|
|
|
5
|
-
| Module
|
|
6
|
-
|
|
7
|
-
| `sessionMiddleware.ts`
|
|
8
|
-
| `authMiddleware.ts`
|
|
9
|
-
| `getSessionCredentials.ts` | Mint STS credentials per connection.
|
|
10
|
-
| `verifyIdToken.ts`
|
|
11
|
-
| `getS3Client.ts`
|
|
12
|
-
| `keycloakAdmin/`
|
|
5
|
+
| Module | Responsibility |
|
|
6
|
+
| -------------------------- | ------------------------------------------------------------------------ |
|
|
7
|
+
| `sessionMiddleware.ts` | Resolve session cookie → session store entry. |
|
|
8
|
+
| `authMiddleware.ts` | Refresh tokens if needed; populate `authContext` for downstream loaders. |
|
|
9
|
+
| `getSessionCredentials.ts` | Mint STS credentials per connection. |
|
|
10
|
+
| `verifyIdToken.ts` | JWKS-based idToken signature/expiry verification. |
|
|
11
|
+
| `getS3Client.ts` | Build an SDK-v3 S3 client with the connection's signed credentials. |
|
|
12
|
+
| `keycloakAdmin/` | Service-account-backed admin API for user/group management. |
|
|
@@ -4,11 +4,7 @@ import { getSessionData } from "./getSession";
|
|
|
4
4
|
import { getAllSessionCredentials } from "./getSessionCredentials";
|
|
5
5
|
import { refreshAccessTokenWithLock } from "./refreshAuthTokens";
|
|
6
6
|
import { sessionContext } from "./sessionMiddleware";
|
|
7
|
-
import {
|
|
8
|
-
type CytarioSession,
|
|
9
|
-
type SessionData,
|
|
10
|
-
sessionStorage,
|
|
11
|
-
} from "./sessionStorage";
|
|
7
|
+
import { type CytarioSession, type SessionData, sessionStorage } from "./sessionStorage";
|
|
12
8
|
import { verifyIdToken } from "./verifyIdToken";
|
|
13
9
|
import { ConnectionConfig } from "~/.generated/client";
|
|
14
10
|
import { createLabel } from "~/.server/logging";
|
|
@@ -52,10 +48,7 @@ const fetchAllCredentials = async (
|
|
|
52
48
|
): Promise<{ sessionData: SessionData; connectionConfigs: ConnectionConfig[] }> => {
|
|
53
49
|
const connectionConfigs = await listConnections(sessionData.user);
|
|
54
50
|
|
|
55
|
-
const newCredentials = await getAllSessionCredentials(
|
|
56
|
-
sessionData,
|
|
57
|
-
connectionConfigs,
|
|
58
|
-
);
|
|
51
|
+
const newCredentials = await getAllSessionCredentials(sessionData, connectionConfigs);
|
|
59
52
|
|
|
60
53
|
return {
|
|
61
54
|
sessionData: {
|
|
@@ -72,18 +65,13 @@ const fetchAllCredentials = async (
|
|
|
72
65
|
* Sets validated session data in authContext for downstream use.
|
|
73
66
|
* Export this from protected routes that require authentication.
|
|
74
67
|
*/
|
|
75
|
-
export const authMiddleware: MiddlewareFunction = async (
|
|
76
|
-
{ request, context },
|
|
77
|
-
next,
|
|
78
|
-
) => {
|
|
68
|
+
export const authMiddleware: MiddlewareFunction = async ({ request, context }, next) => {
|
|
79
69
|
console.info(`${label} ${request.method} ${request.url}`);
|
|
80
70
|
|
|
81
71
|
const session = context.get(sessionContext);
|
|
82
72
|
|
|
83
73
|
if (!session) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"Session not found in context. Ensure sessionMiddleware runs first.",
|
|
86
|
-
);
|
|
74
|
+
throw new Error("Session not found in context. Ensure sessionMiddleware runs first.");
|
|
87
75
|
}
|
|
88
76
|
|
|
89
77
|
const sessionData = await getSessionData(session);
|
|
@@ -116,10 +104,7 @@ export const authMiddleware: MiddlewareFunction = async (
|
|
|
116
104
|
|
|
117
105
|
let newAuthTokens;
|
|
118
106
|
try {
|
|
119
|
-
newAuthTokens = await refreshAccessTokenWithLock(
|
|
120
|
-
session.id,
|
|
121
|
-
authTokens.refreshToken,
|
|
122
|
-
);
|
|
107
|
+
newAuthTokens = await refreshAccessTokenWithLock(session.id, authTokens.refreshToken);
|
|
123
108
|
} catch (error) {
|
|
124
109
|
console.error(`${label} Token refresh failed:`, error);
|
|
125
110
|
}
|
|
@@ -127,11 +112,10 @@ export const authMiddleware: MiddlewareFunction = async (
|
|
|
127
112
|
if (newAuthTokens) {
|
|
128
113
|
session.set("authTokens", newAuthTokens);
|
|
129
114
|
|
|
130
|
-
const { sessionData: withCredentials, connectionConfigs } =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
115
|
+
const { sessionData: withCredentials, connectionConfigs } = await fetchAllCredentials({
|
|
116
|
+
...updatedSessionData,
|
|
117
|
+
authTokens: newAuthTokens,
|
|
118
|
+
});
|
|
135
119
|
updatedSessionData = withCredentials;
|
|
136
120
|
|
|
137
121
|
session.set("credentials", updatedSessionData.credentials);
|
|
@@ -19,9 +19,7 @@ export const exchangeAuthCode = async (
|
|
|
19
19
|
method: "POST",
|
|
20
20
|
headers: {
|
|
21
21
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
22
|
-
Authorization: `Basic ${Buffer.from(
|
|
23
|
-
`${clientId}:${clientSecret}`,
|
|
24
|
-
).toString("base64")}`,
|
|
22
|
+
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
|
25
23
|
},
|
|
26
24
|
body: new URLSearchParams({
|
|
27
25
|
grant_type: "authorization_code",
|
|
@@ -32,9 +30,7 @@ export const exchangeAuthCode = async (
|
|
|
32
30
|
});
|
|
33
31
|
|
|
34
32
|
if (!tokenResponse.ok) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Token exchange failed: ${tokenResponse.status}`,
|
|
37
|
-
);
|
|
33
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
const json = await tokenResponse.json();
|
|
@@ -24,17 +24,11 @@ const s3ClientCache = new LRUCache<string, CacheEntry>({
|
|
|
24
24
|
/**
|
|
25
25
|
* Creates a unique cache key scoped to user, bucket, and credential identity.
|
|
26
26
|
*/
|
|
27
|
-
const createCacheKey = (
|
|
28
|
-
userId: string,
|
|
29
|
-
bucketName: string,
|
|
30
|
-
credentials: Credentials,
|
|
31
|
-
): string => {
|
|
27
|
+
const createCacheKey = (userId: string, bucketName: string, credentials: Credentials): string => {
|
|
32
28
|
// Hash credentials to detect when they change (e.g., after refresh)
|
|
33
29
|
const credHash = crypto
|
|
34
30
|
.createHash("sha256")
|
|
35
|
-
.update(
|
|
36
|
-
`${credentials.AccessKeyId}:${credentials.SecretAccessKey}:${credentials.SessionToken}`,
|
|
37
|
-
)
|
|
31
|
+
.update(`${credentials.AccessKeyId}:${credentials.SecretAccessKey}:${credentials.SessionToken}`)
|
|
38
32
|
.digest("hex")
|
|
39
33
|
.substring(0, 16);
|
|
40
34
|
|
|
@@ -89,10 +83,7 @@ export const getS3Client = async (
|
|
|
89
83
|
};
|
|
90
84
|
|
|
91
85
|
/** Remove all cached S3 clients for a given user + bucket (e.g. after config change). */
|
|
92
|
-
export const invalidateS3ClientsForBucket = (
|
|
93
|
-
userId: string,
|
|
94
|
-
bucketName: string,
|
|
95
|
-
): void => {
|
|
86
|
+
export const invalidateS3ClientsForBucket = (userId: string, bucketName: string): void => {
|
|
96
87
|
for (const key of s3ClientCache.keys()) {
|
|
97
88
|
const entry = s3ClientCache.peek(key);
|
|
98
89
|
if (entry && entry.userId === userId && entry.bucketName === bucketName) {
|
|
@@ -100,4 +91,3 @@ export const invalidateS3ClientsForBucket = (
|
|
|
100
91
|
}
|
|
101
92
|
}
|
|
102
93
|
};
|
|
103
|
-
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
STSClient,
|
|
5
|
-
} from "@aws-sdk/client-sts";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
type ConnectionsCredentials,
|
|
9
|
-
type SessionData,
|
|
10
|
-
} from "./sessionStorage";
|
|
1
|
+
import { AssumeRoleWithWebIdentityCommand, Credentials, STSClient } from "@aws-sdk/client-sts";
|
|
2
|
+
|
|
3
|
+
import { type ConnectionsCredentials, type SessionData } from "./sessionStorage";
|
|
11
4
|
import { ConnectionConfig } from "~/.generated/client";
|
|
12
5
|
import { createLabel } from "~/.server/logging";
|
|
13
6
|
import { getS3ProviderConfig } from "~/utils/s3Provider";
|
|
14
7
|
|
|
15
8
|
const label = createLabel("credentials", "cyan");
|
|
16
9
|
|
|
17
|
-
export const isValidCredentials = (
|
|
18
|
-
credentials?: { Expiration?: Date },
|
|
19
|
-
): boolean => {
|
|
10
|
+
export const isValidCredentials = (credentials?: { Expiration?: Date }): boolean => {
|
|
20
11
|
if (!credentials?.Expiration) return false;
|
|
21
12
|
|
|
22
13
|
// Check if credentials are expired (with 5 minute buffer)
|
|
@@ -91,9 +82,7 @@ export const getAllSessionCredentials = async (
|
|
|
91
82
|
return sessionData.credentials;
|
|
92
83
|
}
|
|
93
84
|
|
|
94
|
-
console.info(
|
|
95
|
-
`${label} Fetching credentials for ${stale.length} connection(s)`,
|
|
96
|
-
);
|
|
85
|
+
console.info(`${label} Fetching credentials for ${stale.length} connection(s)`);
|
|
97
86
|
|
|
98
87
|
const roleSessionName = sanitizeRoleSessionName(sessionData.user.name);
|
|
99
88
|
|
|
@@ -114,10 +103,7 @@ export const getAllSessionCredentials = async (
|
|
|
114
103
|
if (result.status === "fulfilled") {
|
|
115
104
|
newCredentials[result.value.name] = result.value.credentials;
|
|
116
105
|
} else {
|
|
117
|
-
console.warn(
|
|
118
|
-
`${label} Failed to fetch credentials for a connection:`,
|
|
119
|
-
result.reason,
|
|
120
|
-
);
|
|
106
|
+
console.warn(`${label} Failed to fetch credentials for a connection:`, result.reason);
|
|
121
107
|
}
|
|
122
108
|
}
|
|
123
109
|
|
|
@@ -48,9 +48,7 @@ function enrichUserProfile(raw: UserProfileRaw): UserProfile {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Retrieves and enriches user profile data from Keycloak. */
|
|
51
|
-
export const getUserInfo = async (
|
|
52
|
-
accessToken: string,
|
|
53
|
-
): Promise<UserProfile> => {
|
|
51
|
+
export const getUserInfo = async (accessToken: string): Promise<UserProfile> => {
|
|
54
52
|
try {
|
|
55
53
|
const wellKnownEndpoints = await getWellKnownEndpoints();
|
|
56
54
|
const { userinfo_endpoint } = wellKnownEndpoints;
|
|
@@ -63,9 +61,7 @@ export const getUserInfo = async (
|
|
|
63
61
|
|
|
64
62
|
if (!response.ok) {
|
|
65
63
|
const errorText = await response.text();
|
|
66
|
-
throw new Error(
|
|
67
|
-
`UserInfo fetch failed: ${response.status} - ${errorText}`,
|
|
68
|
-
);
|
|
64
|
+
throw new Error(`UserInfo fetch failed: ${response.status} - ${errorText}`);
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
const rawUserProfile = await response.json();
|
|
@@ -27,16 +27,9 @@ export class KeycloakAdminError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const adminApiBaseUrl = cytarioConfig.auth.baseUrl.replace(
|
|
31
|
-
"/realms/",
|
|
32
|
-
"/admin/realms/",
|
|
33
|
-
);
|
|
30
|
+
const adminApiBaseUrl = cytarioConfig.auth.baseUrl.replace("/realms/", "/admin/realms/");
|
|
34
31
|
|
|
35
|
-
async function adminRequest(
|
|
36
|
-
method: string,
|
|
37
|
-
path: string,
|
|
38
|
-
body?: unknown,
|
|
39
|
-
): Promise<Response> {
|
|
32
|
+
async function adminRequest(method: string, path: string, body?: unknown): Promise<Response> {
|
|
40
33
|
const accessToken = await getAdminToken();
|
|
41
34
|
|
|
42
35
|
const response = await fetch(`${adminApiBaseUrl}${path}`, {
|
|
@@ -61,9 +61,7 @@ export function flattenGroups(groups: KeycloakGroup[]): string[] {
|
|
|
61
61
|
* For each admin scope, finds the exact group by path and flattens its descendants.
|
|
62
62
|
* Uses findGroupByPath instead of search to avoid returning unrelated groups.
|
|
63
63
|
*/
|
|
64
|
-
export async function getManageableScopes(
|
|
65
|
-
user: UserProfile,
|
|
66
|
-
): Promise<string[]> {
|
|
64
|
+
export async function getManageableScopes(user: UserProfile): Promise<string[]> {
|
|
67
65
|
if (user.adminScopes.length === 0) return [];
|
|
68
66
|
|
|
69
67
|
const allScopes = new Set<string>();
|
|
@@ -78,10 +76,7 @@ export async function getManageableScopes(
|
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
} catch (error) {
|
|
81
|
-
console.warn(
|
|
82
|
-
`Failed to fetch group tree for admin scope "${adminScope}":`,
|
|
83
|
-
error,
|
|
84
|
-
);
|
|
79
|
+
console.warn(`Failed to fetch group tree for admin scope "${adminScope}":`, error);
|
|
85
80
|
}
|
|
86
81
|
}
|
|
87
82
|
|
|
@@ -91,9 +86,7 @@ export async function getManageableScopes(
|
|
|
91
86
|
/**
|
|
92
87
|
* Finds a Keycloak group by its normalized path (e.g. "cytario/lab").
|
|
93
88
|
*/
|
|
94
|
-
export async function findGroupByPath(
|
|
95
|
-
scope: string,
|
|
96
|
-
): Promise<KeycloakGroup | undefined> {
|
|
89
|
+
export async function findGroupByPath(scope: string): Promise<KeycloakGroup | undefined> {
|
|
97
90
|
const topLevel = scope.split("/")[0];
|
|
98
91
|
const groups = await fetchGroups(topLevel);
|
|
99
92
|
const targetPath = `/${scope}`;
|
|
@@ -185,11 +178,7 @@ export async function createGroup(
|
|
|
185
178
|
throw new KeycloakAdminError(404, `Parent group not found: ${parentScope}`);
|
|
186
179
|
}
|
|
187
180
|
|
|
188
|
-
const response = await adminMutate(
|
|
189
|
-
"POST",
|
|
190
|
-
`/groups/${parent.id}/children`,
|
|
191
|
-
{ name },
|
|
192
|
-
);
|
|
181
|
+
const response = await adminMutate("POST", `/groups/${parent.id}/children`, { name });
|
|
193
182
|
|
|
194
183
|
const location = response.headers.get("location");
|
|
195
184
|
if (!location) {
|
|
@@ -202,11 +191,9 @@ export async function createGroup(
|
|
|
202
191
|
|
|
203
192
|
let adminsGroupId: string;
|
|
204
193
|
try {
|
|
205
|
-
const adminsResponse = await adminMutate(
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
{ name: "admins" },
|
|
209
|
-
);
|
|
194
|
+
const adminsResponse = await adminMutate("POST", `/groups/${newGroupId}/children`, {
|
|
195
|
+
name: "admins",
|
|
196
|
+
});
|
|
210
197
|
const adminsLocation = adminsResponse.headers.get("location");
|
|
211
198
|
adminsGroupId = adminsLocation?.split("/").pop() ?? "";
|
|
212
199
|
if (!adminsGroupId) {
|
|
@@ -232,9 +219,7 @@ export async function createGroup(
|
|
|
232
219
|
/**
|
|
233
220
|
* Fetches members for a group and all its sub-groups, returning the tree structure.
|
|
234
221
|
*/
|
|
235
|
-
export async function getGroupWithMembers(
|
|
236
|
-
scope: string,
|
|
237
|
-
): Promise<GroupWithMembers | undefined> {
|
|
222
|
+
export async function getGroupWithMembers(scope: string): Promise<GroupWithMembers | undefined> {
|
|
238
223
|
const group = await findGroupByPath(scope);
|
|
239
224
|
if (!group) return undefined;
|
|
240
225
|
|
|
@@ -243,9 +228,7 @@ export async function getGroupWithMembers(
|
|
|
243
228
|
|
|
244
229
|
await Promise.all(
|
|
245
230
|
allIds.map(async (id) => {
|
|
246
|
-
const members = await adminFetch<KeycloakUser[]>(
|
|
247
|
-
`/groups/${id}/members?max=500`,
|
|
248
|
-
);
|
|
231
|
+
const members = await adminFetch<KeycloakUser[]>(`/groups/${id}/members?max=500`);
|
|
249
232
|
membersByGroupId.set(id, members);
|
|
250
233
|
}),
|
|
251
234
|
);
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
adminFetch,
|
|
3
|
-
adminMutate,
|
|
4
|
-
KeycloakAdminError,
|
|
5
|
-
type KeycloakUser,
|
|
6
|
-
} from "./client";
|
|
1
|
+
import { adminFetch, adminMutate, KeycloakAdminError, type KeycloakUser } from "./client";
|
|
7
2
|
import { findGroupByPath } from "./groups";
|
|
8
3
|
import { cytarioConfig } from "~/config";
|
|
9
4
|
|
|
@@ -18,24 +13,15 @@ export async function updateUser(
|
|
|
18
13
|
await adminMutate("PUT", `/users/${userId}`, data);
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
export async function addUserToGroup(
|
|
22
|
-
userId: string,
|
|
23
|
-
groupId: string,
|
|
24
|
-
): Promise<void> {
|
|
16
|
+
export async function addUserToGroup(userId: string, groupId: string): Promise<void> {
|
|
25
17
|
await adminMutate("PUT", `/users/${userId}/groups/${groupId}`);
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
export async function removeUserFromGroup(
|
|
29
|
-
userId: string,
|
|
30
|
-
groupId: string,
|
|
31
|
-
): Promise<void> {
|
|
20
|
+
export async function removeUserFromGroup(userId: string, groupId: string): Promise<void> {
|
|
32
21
|
await adminMutate("DELETE", `/users/${userId}/groups/${groupId}`);
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
export async function setUserEnabled(
|
|
36
|
-
userId: string,
|
|
37
|
-
enabled: boolean,
|
|
38
|
-
): Promise<void> {
|
|
24
|
+
export async function setUserEnabled(userId: string, enabled: boolean): Promise<void> {
|
|
39
25
|
await adminMutate("PUT", `/users/${userId}`, { enabled });
|
|
40
26
|
}
|
|
41
27
|
|
|
@@ -84,10 +70,8 @@ export async function inviteUser(
|
|
|
84
70
|
client_id: cytarioConfig.auth.clientId,
|
|
85
71
|
redirect_uri: cytarioConfig.endpoints.webapp,
|
|
86
72
|
});
|
|
87
|
-
await adminMutate(
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
["UPDATE_PASSWORD"],
|
|
91
|
-
);
|
|
73
|
+
await adminMutate("PUT", `/users/${userId}/execute-actions-email?${params}`, [
|
|
74
|
+
"UPDATE_PASSWORD",
|
|
75
|
+
]);
|
|
92
76
|
}
|
|
93
77
|
}
|
|
@@ -23,8 +23,7 @@ export interface OAuthStateResult {
|
|
|
23
23
|
* Generates a PKCE code verifier (RFC 7636 §4.1).
|
|
24
24
|
* 43-char base64url string from 32 random bytes.
|
|
25
25
|
*/
|
|
26
|
-
export const generateCodeVerifier = (): string =>
|
|
27
|
-
randomBytes(32).toString("base64url");
|
|
26
|
+
export const generateCodeVerifier = (): string => randomBytes(32).toString("base64url");
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Generates a PKCE code challenge (S256) from a code verifier (RFC 7636 §4.2).
|
|
@@ -59,9 +58,7 @@ export const validateRedirectTo = (redirectTo?: string): string => {
|
|
|
59
58
|
* and stores it in the cache store (Redis/Valkey) with a short expiry time.
|
|
60
59
|
* Returns state, codeChallenge, and nonce for the authorization URL.
|
|
61
60
|
*/
|
|
62
|
-
export const generateOAuthState = async (
|
|
63
|
-
redirectTo?: string,
|
|
64
|
-
): Promise<OAuthStateResult> => {
|
|
61
|
+
export const generateOAuthState = async (redirectTo?: string): Promise<OAuthStateResult> => {
|
|
65
62
|
const state = randomBytes(32).toString("hex");
|
|
66
63
|
const codeVerifier = generateCodeVerifier();
|
|
67
64
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
@@ -75,11 +72,7 @@ export const generateOAuthState = async (
|
|
|
75
72
|
nonce,
|
|
76
73
|
};
|
|
77
74
|
|
|
78
|
-
await redis.setex(
|
|
79
|
-
`${STATE_PREFIX}${state}`,
|
|
80
|
-
STATE_EXPIRY_SECONDS,
|
|
81
|
-
JSON.stringify(stateData),
|
|
82
|
-
);
|
|
75
|
+
await redis.setex(`${STATE_PREFIX}${state}`, STATE_EXPIRY_SECONDS, JSON.stringify(stateData));
|
|
83
76
|
|
|
84
77
|
return { state, codeChallenge, nonce };
|
|
85
78
|
};
|
|
@@ -89,9 +82,7 @@ export const generateOAuthState = async (
|
|
|
89
82
|
* Returns the state data if valid, or null if invalid/expired.
|
|
90
83
|
* Uses atomic GETDEL to prevent reuse (requires Redis 6.2+ / Valkey).
|
|
91
84
|
*/
|
|
92
|
-
export const validateOAuthState = async (
|
|
93
|
-
state: string,
|
|
94
|
-
): Promise<OAuthState | null> => {
|
|
85
|
+
export const validateOAuthState = async (state: string): Promise<OAuthState | null> => {
|
|
95
86
|
const key = `${STATE_PREFIX}${state}`;
|
|
96
87
|
const stateJson = await redis.getdel(key);
|
|
97
88
|
|
|
@@ -5,9 +5,7 @@ import { getSession, getSessionData } from "~/.server/auth/getSession";
|
|
|
5
5
|
/**
|
|
6
6
|
* Redirects the user to the profile page if they are already authenticated.
|
|
7
7
|
*/
|
|
8
|
-
export const redirectIfAuthenticated = async ({
|
|
9
|
-
request,
|
|
10
|
-
}: LoaderFunctionArgs): Promise<void> => {
|
|
8
|
+
export const redirectIfAuthenticated = async ({ request }: LoaderFunctionArgs): Promise<void> => {
|
|
11
9
|
const session = await getSession(request);
|
|
12
10
|
const { user } = await getSessionData(session);
|
|
13
11
|
|
|
@@ -18,9 +18,7 @@ export interface AuthTokensResponse {
|
|
|
18
18
|
/**
|
|
19
19
|
* Refreshes the access token using the provided refresh token.
|
|
20
20
|
*/
|
|
21
|
-
export async function refreshAccessToken(
|
|
22
|
-
refreshToken: string,
|
|
23
|
-
): Promise<AuthTokens> {
|
|
21
|
+
export async function refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
|
|
24
22
|
const wellKnownEndpoints = await getWellKnownEndpoints();
|
|
25
23
|
|
|
26
24
|
const response = await fetch(wellKnownEndpoints.token_endpoint, {
|
|
@@ -34,8 +32,7 @@ export async function refreshAccessToken(
|
|
|
34
32
|
}),
|
|
35
33
|
});
|
|
36
34
|
if (!response.ok) throw new Error("Failed to refresh token");
|
|
37
|
-
const { access_token, id_token, refresh_token } =
|
|
38
|
-
(await response.json()) as AuthTokensResponse;
|
|
35
|
+
const { access_token, id_token, refresh_token } = (await response.json()) as AuthTokensResponse;
|
|
39
36
|
|
|
40
37
|
const authTokens: AuthTokens = {
|
|
41
38
|
accessToken: access_token,
|
|
@@ -69,9 +66,7 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
69
66
|
* Reads the current session auth tokens directly from Redis,
|
|
70
67
|
* bypassing the in-memory LRU cache to get the freshest data.
|
|
71
68
|
*/
|
|
72
|
-
async function readSessionTokensFromStore(
|
|
73
|
-
sessionId: string,
|
|
74
|
-
): Promise<AuthTokens | null> {
|
|
69
|
+
async function readSessionTokensFromStore(sessionId: string): Promise<AuthTokens | null> {
|
|
75
70
|
const data = await redis.hget(sessionId, "data");
|
|
76
71
|
if (!data) return null;
|
|
77
72
|
|
|
@@ -96,13 +91,7 @@ export async function refreshAccessTokenWithLock(
|
|
|
96
91
|
const lockValue = randomUUID();
|
|
97
92
|
|
|
98
93
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
99
|
-
const acquired = await redis.set(
|
|
100
|
-
lockKey,
|
|
101
|
-
lockValue,
|
|
102
|
-
"EX",
|
|
103
|
-
LOCK_TTL_SECONDS,
|
|
104
|
-
"NX",
|
|
105
|
-
);
|
|
94
|
+
const acquired = await redis.set(lockKey, lockValue, "EX", LOCK_TTL_SECONDS, "NX");
|
|
106
95
|
|
|
107
96
|
if (acquired === "OK") {
|
|
108
97
|
try {
|
|
@@ -110,10 +99,7 @@ export async function refreshAccessTokenWithLock(
|
|
|
110
99
|
// by a previous lock holder (handles refresh token rotation)
|
|
111
100
|
const currentTokens = await readSessionTokensFromStore(sessionId);
|
|
112
101
|
|
|
113
|
-
if (
|
|
114
|
-
currentTokens &&
|
|
115
|
-
currentTokens.refreshToken !== refreshToken
|
|
116
|
-
) {
|
|
102
|
+
if (currentTokens && currentTokens.refreshToken !== refreshToken) {
|
|
117
103
|
return currentTokens;
|
|
118
104
|
}
|
|
119
105
|
|
|
@@ -9,10 +9,7 @@ export const sessionContext = createContext<CytarioSession>();
|
|
|
9
9
|
* Middleware that retrieves the session from the request
|
|
10
10
|
* and sets it in the context for downstream use.
|
|
11
11
|
*/
|
|
12
|
-
export const sessionMiddleware: MiddlewareFunction = async (
|
|
13
|
-
{ request, context },
|
|
14
|
-
next
|
|
15
|
-
) => {
|
|
12
|
+
export const sessionMiddleware: MiddlewareFunction = async ({ request, context }, next) => {
|
|
16
13
|
const session = await getSession(request);
|
|
17
14
|
context.set(sessionContext, session);
|
|
18
15
|
return next();
|
|
@@ -47,10 +47,7 @@ const sessionCache = new LRUCache<string, SessionData>({
|
|
|
47
47
|
ttl: 5000, // 5 seconds TTL - short enough to keep data fresh
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
export const sessionStorage = createSessionStorage<
|
|
51
|
-
SessionData,
|
|
52
|
-
SessionFlashData
|
|
53
|
-
>({
|
|
50
|
+
export const sessionStorage = createSessionStorage<SessionData, SessionFlashData>({
|
|
54
51
|
cookie: {
|
|
55
52
|
name: "__session",
|
|
56
53
|
...cookie,
|
|
@@ -20,9 +20,7 @@ const getJwks = async () => {
|
|
|
20
20
|
* behavior varies by client configuration. Add after confirming the actual
|
|
21
21
|
* `aud` claim value in production tokens.
|
|
22
22
|
*/
|
|
23
|
-
export const verifyIdToken = async (
|
|
24
|
-
token: string,
|
|
25
|
-
): Promise<JWTPayload | null> => {
|
|
23
|
+
export const verifyIdToken = async (token: string): Promise<JWTPayload | null> => {
|
|
26
24
|
try {
|
|
27
25
|
const jwks = await getJwks();
|
|
28
26
|
const { issuer } = await getWellKnownEndpoints();
|
|
@@ -32,10 +32,7 @@ const fetchWellKnownEndpoints = async (): Promise<WellKnownEndpoints> => {
|
|
|
32
32
|
cacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
|
33
33
|
return data;
|
|
34
34
|
} catch (error) {
|
|
35
|
-
console.error(
|
|
36
|
-
`Error fetching well-known endpoints from ${endpointUrl}:`,
|
|
37
|
-
error,
|
|
38
|
-
);
|
|
35
|
+
console.error(`Error fetching well-known endpoints from ${endpointUrl}:`, error);
|
|
39
36
|
cachedEndpoints = null;
|
|
40
37
|
cacheExpiresAt = 0;
|
|
41
38
|
throw error;
|
package/app/.server/db/redis.ts
CHANGED
|
@@ -59,6 +59,10 @@ redis.on("error", (err) => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
redis.on("connect", () => {
|
|
62
|
-
const authInfo = username
|
|
62
|
+
const authInfo = username
|
|
63
|
+
? ` (authenticated as ${username})`
|
|
64
|
+
: password
|
|
65
|
+
? " (authenticated)"
|
|
66
|
+
: "";
|
|
63
67
|
console.log(`Connected to Redis/Valkey at ${host}:${port}${authInfo}`);
|
|
64
68
|
});
|
package/app/.server/logging.ts
CHANGED
|
@@ -9,8 +9,5 @@ const ansiColors = {
|
|
|
9
9
|
gray: "\x1b[90m",
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const createLabel = (
|
|
13
|
-
str: string,
|
|
14
|
-
color: keyof typeof ansiColors = "white"
|
|
15
|
-
) =>
|
|
12
|
+
export const createLabel = (str: string, color: keyof typeof ansiColors = "white") =>
|
|
16
13
|
`${ansiColors[color]}[${str.slice(0, 10).toUpperCase().padEnd(10, " ")}]\x1b[0m`;
|
|
@@ -8,10 +8,7 @@ const label = createLabel("duration", "red");
|
|
|
8
8
|
* Middleware that logs request duration for debugging performance.
|
|
9
9
|
* Export this from routes where you want to measure request timing.
|
|
10
10
|
*/
|
|
11
|
-
export const requestDurationMiddleware: MiddlewareFunction = async (
|
|
12
|
-
{ request },
|
|
13
|
-
next
|
|
14
|
-
) => {
|
|
11
|
+
export const requestDurationMiddleware: MiddlewareFunction = async ({ request }, next) => {
|
|
15
12
|
const startTime = performance.now();
|
|
16
13
|
const url = new URL(request.url);
|
|
17
14
|
const path = url.pathname + url.search;
|