diff --git a/.env.example b/.env.example
index 36d21c06c2..43e6b06197 100644
--- a/.env.example
+++ b/.env.example
@@ -4,4 +4,8 @@ PERSISTENT_DIR=/data
# macOS Development Setup - Fixes issues with elastic search container - set to ES_JAVA_OPTS=-XX:UseSVE=0 -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true MEM_LIMIT=2g SECOMP="unconfined"
MEM_LIMIT=
ES_JAVA_OPTS=
-SECOMP=
\ No newline at end of file
+SECOMP=
+
+# coral --------------------------------------------------------------------
+
+CORAL_SIGNING_SECRET=""
\ No newline at end of file
diff --git a/config.env.example b/config.env.example
index 1b826fced2..6b989b98ca 100644
--- a/config.env.example
+++ b/config.env.example
@@ -4,6 +4,9 @@ CRN_SERVER_URL=http://localhost:9876
# Secret authentication value for this instance
JWT_SECRET=
+# Coral Talk Secret from SSO Config in Admin Panel
+CORAL_JWT_SECRET=
+
# Google oauth2 configuration - configure to enable Google auth
# https://developers.google.com/identity/sign-in/web/sign-in for instructions
GOOGLE_CLIENT_ID=
diff --git a/docker-compose.yml b/docker-compose.yml
index 52b2cfae62..311c61f59e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -165,3 +165,17 @@ services:
ports:
- "9200:9200"
- "9300:9300"
+
+ # Coral Project Talk
+ coral:
+ image: coralproject/talk:7
+ restart: always
+ ports:
+ - "5001:5000"
+ depends_on:
+ - mongo
+ - redis
+ environment:
+ - MONGODB_URI=mongodb://mongo:27017/coral
+ - REDIS_URI=redis://redis:6379
+ - SIGNING_SECRET=${CORAL_SIGNING_SECRET}
diff --git a/nginx/nginx.dev.conf b/nginx/nginx.dev.conf
index c14087c91e..044a477dcd 100644
--- a/nginx/nginx.dev.conf
+++ b/nginx/nginx.dev.conf
@@ -74,6 +74,19 @@ server {
alias /content;
}
+
+
+ # Specific proxy for ONLY embed.js from Coral
+ location /coral/assets/js/embed.js {
+ client_max_body_size 0;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Connection "";
+ proxy_http_version 1.1;
+ proxy_request_buffering off;
+ proxy_pass http://coral:5000/assets/js/embed.js;
+ }
+
# crn-web app
root /srv/app/dist;
diff --git a/packages/openneuro-app/src/@types/custom.d.ts b/packages/openneuro-app/src/@types/custom.d.ts
index c2754522bb..3ec1254187 100644
--- a/packages/openneuro-app/src/@types/custom.d.ts
+++ b/packages/openneuro-app/src/@types/custom.d.ts
@@ -45,6 +45,9 @@ interface Blob {
interface Window {
showDirectoryPicker: any
+ Coral?: {
+ createStreamEmbed: (config: any) => void
+ }
}
interface Navigator {
diff --git a/packages/openneuro-app/src/assets/coral/coral-embed.css b/packages/openneuro-app/src/assets/coral/coral-embed.css
new file mode 100644
index 0000000000..29b6e4fc56
--- /dev/null
+++ b/packages/openneuro-app/src/assets/coral/coral-embed.css
@@ -0,0 +1,60 @@
+#coral {
+ --palette-primary-200: #b5cbd3;
+ --palette-primary-400: #377ed2;
+ --palette-primary-500: #1c4550;
+ --palette-primary-700: #163a45;
+ --palette-primary-800: #102f38;
+ --palette-primary-900: #0b252c;
+
+ --palette-error-600: #cc322f;
+ --palette-error-700: #b32c29;
+ --palette-error-800: #992523;
+ --palette-error-900: #7f1f1d;
+
+ --palette-success-600: #28b463;
+ --palette-success-700: #239b56;
+ --palette-success-800: #1d8348;
+ --palette-success-900: #186a3b;
+
+ /* FONTS */
+ --font-family-primary: 'Open Sans', sans-serif;
+ --font-family-secondary: 'Nunito', sans-serif;
+
+ --font-weight-primary-bold: 700;
+ --font-weight-primary-semi-bold: 600;
+ --font-weight-primary-regular: 500;
+
+ --font-weight-secondary-bold: 700;
+ --font-weight-secondary-regular: 500;
+
+ /* SHADOWS */
+ --shadow-popover: 1px 0px 4px rgba(0, 0, 0, 0.25);
+}
+
+
+#coral .coral.coral-viewerBox{
+ display: none;
+}
+
+#coral{
+ --palette-primary-500: #1c4550;
+}
+
+#coral.meg{
+ --palette-primary-500:rgba(156, 57, 0, 1);
+}
+
+ #coral.eeg{
+ --palette-primary-500:rgba(134, 31, 55, 1);
+}
+
+ #coral.pet{
+ --palette-primary-500:rgba(0, 105, 192, 1);
+}
+
+ #coral.ieeg{
+ --palette-primary-500:rgba(18, 109, 62, 1);
+}
+#coral.mri{
+ --palette-primary-500:rgba(79, 51, 130, 1);
+}
diff --git a/packages/openneuro-app/src/client.jsx b/packages/openneuro-app/src/client.jsx
index ec9ef7ef08..991e80a42b 100644
--- a/packages/openneuro-app/src/client.jsx
+++ b/packages/openneuro-app/src/client.jsx
@@ -1,7 +1,6 @@
/**
* Browser client entrypoint
*/
-import "./scripts/utils/global-polyfill"
import "./scripts/sentry"
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"
import React from "react"
diff --git a/packages/openneuro-app/src/index.html b/packages/openneuro-app/src/index.html
index 95eb2028d2..a6e7559567 100644
--- a/packages/openneuro-app/src/index.html
+++ b/packages/openneuro-app/src/index.html
@@ -18,6 +18,10 @@
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"
rel="stylesheet"
/>
+
diff --git a/packages/openneuro-app/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap b/packages/openneuro-app/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap
index 3dfe36b137..6acdd2351e 100644
--- a/packages/openneuro-app/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap
+++ b/packages/openneuro-app/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap
@@ -747,15 +747,8 @@ OCI-1131441 (R. Poldrack, PI) in any publications.
+ id="coral_thread"
+ />
({
+ UserModalOpenCtx: React.createContext({ setUserModalOpen: vi.fn() }),
+}))
+
+global.fetch = vi.fn().mockResolvedValue(Promise.resolve({
+ json: vi.fn().mockResolvedValue({ token: "mock-sso-token" }),
+}))
+
+// Mock the global Coral object
+const mockCoralCreateStreamEmbed = vi.fn()
+global.window.Coral = {
+ createStreamEmbed: mockCoralCreateStreamEmbed,
+}
+
+describe("CoralEmbed - Simple Embed.js Check (Vitest)", () => {
+ const mockStoryID = "simple-test-story"
+ const mockModalities = ["test"]
+
+ it("should check if window.Coral exists and initializeCoralEmbed is called", async () => {
+ render(
+
+
+ ,
+ )
+
+ // fetch and Coral initialization
+ await waitFor(() => {
+ expect(global.window.Coral).toBeDefined()
+ expect(mockCoralCreateStreamEmbed).toHaveBeenCalledTimes(1)
+ })
+
+ expect(mockCoralCreateStreamEmbed).toHaveBeenCalledWith(
+ expect.objectContaining({
+ storyID: mockStoryID,
+ }),
+ )
+ })
+})
diff --git a/packages/openneuro-app/src/scripts/dataset/comments/coral-embed.tsx b/packages/openneuro-app/src/scripts/dataset/comments/coral-embed.tsx
new file mode 100644
index 0000000000..9f33ca2b4d
--- /dev/null
+++ b/packages/openneuro-app/src/scripts/dataset/comments/coral-embed.tsx
@@ -0,0 +1,56 @@
+import React, { useContext, useEffect, useRef } from "react"
+import * as Sentry from "@sentry/react"
+import { isAdmin } from "../../authentication/admin-user"
+import { UserModalOpenCtx } from "../../utils/user-login-modal-ctx"
+
+export const CoralEmbed: React.FC<{ storyID: string; modalities: string[] }> = (
+ { storyID, modalities },
+) => {
+ const coralContainerRef = useRef
(null)
+ const isAdminUser = isAdmin()
+ const { setUserModalOpen } = useContext(UserModalOpenCtx)
+
+ useEffect(() => {
+ const fetchAndInitializeCoral = async () => {
+ const accessToken = document.cookie
+ .split("; ")
+ .find((cookie) => cookie.startsWith("accessToken="))
+ ?.split("=")[1]
+ const headers = accessToken
+ ? { Authorization: `Bearer ${accessToken}` }
+ : {}
+
+ try {
+ const response = await fetch("/api/auth/coral-sso", { headers })
+ const data = await response.json()
+ initializeCoralEmbed(data.token)
+ } catch (error) {
+ Sentry.captureException(error)
+ initializeCoralEmbed(undefined)
+ }
+ }
+
+ const initializeCoralEmbed = (token: string | undefined) => {
+ if (coralContainerRef.current && window.Coral) {
+ window.Coral.createStreamEmbed({
+ id: coralContainerRef.current.id,
+ autoRender: true,
+ rootURL: "http://localhost:5001",
+ storyID: storyID,
+ storyURL: window.location.href,
+ accessToken: token,
+ role: isAdminUser ? "ADMIN" : "COMMENTER",
+ containerClassName: modalities,
+ events: function (events) {
+ events.on("loginPrompt", function () {
+ setUserModalOpen(true)
+ })
+ },
+ })
+ }
+ }
+ fetchAndInitializeCoral()
+ }, [storyID, isAdminUser, setUserModalOpen])
+
+ return
+}
diff --git a/packages/openneuro-app/src/scripts/dataset/routes/dataset-default.tsx b/packages/openneuro-app/src/scripts/dataset/routes/dataset-default.tsx
index 388ffdd288..3027ec81e7 100644
--- a/packages/openneuro-app/src/scripts/dataset/routes/dataset-default.tsx
+++ b/packages/openneuro-app/src/scripts/dataset/routes/dataset-default.tsx
@@ -3,49 +3,50 @@ import { Markdown } from "../../utils/markdown"
import { ReadMore } from "../../components/read-more/ReadMore"
import { MetaDataBlock } from "../components/MetaDataBlock"
import Files from "../files/files"
-import Comments from "../comments/comments"
import EditDescriptionField from "../fragments/edit-description-field"
+import { CoralEmbed } from "../comments/coral-embed"
/**
* Default tab for dataset draft pages
*/
-export const DatasetDefault = ({ dataset, hasEdit }) => (
- <>
- (
-
- {
+ return (
+ <>
+ (
+
- {dataset.draft.readme || "N/A"}
-
-
- )}
- />
-
-
- >
-)
+
+ {dataset.draft.readme || "N/A"}
+
+
+ )}
+ />
+
+
+ >
+ )
+}
diff --git a/packages/openneuro-app/src/scripts/dataset/routes/snapshot-default.tsx b/packages/openneuro-app/src/scripts/dataset/routes/snapshot-default.tsx
index 9f86d930c0..ffba8a99fb 100644
--- a/packages/openneuro-app/src/scripts/dataset/routes/snapshot-default.tsx
+++ b/packages/openneuro-app/src/scripts/dataset/routes/snapshot-default.tsx
@@ -3,7 +3,7 @@ import { Markdown } from "../../utils/markdown"
import { ReadMore } from "../../components/read-more/ReadMore"
import { MetaDataBlock } from "../components/MetaDataBlock"
import Files from "../files/files"
-import Comments from "../comments/comments"
+import { CoralEmbed } from "../comments/coral-embed"
/**
* Default tab for snapshot pages
@@ -30,10 +30,9 @@ export const SnapshotDefault = ({ dataset, snapshot }) => (
datasetPermissions={dataset.permissions}
summary={snapshot?.summary}
/>
-
>
)
diff --git a/packages/openneuro-app/src/scripts/utils/global-polyfill.ts b/packages/openneuro-app/src/scripts/utils/global-polyfill.ts
deleted file mode 100644
index 6eb475d19e..0000000000
--- a/packages/openneuro-app/src/scripts/utils/global-polyfill.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/* eslint-disable */
-// Workaround for incorrect global reference in draft.js via fbjs
-interface Object {
- // eslint-disable-next-line @typescript-eslint/ban-types
- global: object
-}
-
-function polyfillGlobal(): void {
- if (typeof global === "object") return
- Object.defineProperty(Object.prototype, "global", {
- get: () => globalThis,
- configurable: true,
- })
-}
-
-polyfillGlobal()
diff --git a/packages/openneuro-server/src/config.ts b/packages/openneuro-server/src/config.ts
index aacb7b2e56..8cafd9a2f2 100644
--- a/packages/openneuro-server/src/config.ts
+++ b/packages/openneuro-server/src/config.ts
@@ -34,6 +34,9 @@ const config = {
jwt: {
secret: process.env.JWT_SECRET,
},
+ coral: {
+ secret: process.env.CORAL_JWT_SECRET,
+ },
},
mongo: {
url: process.env.MONGO_URL,
diff --git a/packages/openneuro-server/src/handlers/users.ts b/packages/openneuro-server/src/handlers/users.ts
index 9f5a52b2f5..c472c1fc6d 100644
--- a/packages/openneuro-server/src/handlers/users.ts
+++ b/packages/openneuro-server/src/handlers/users.ts
@@ -1,6 +1,9 @@
// dependencies ------------------------------------------------------------
+import * as Sentry from "@sentry/node"
import { generateApiKey } from "../libs/apikey"
-
+import jwt from "jsonwebtoken"
+import config from "../config"
+import { v4 as uuidv4 } from "uuid"
// handlers ----------------------------------------------------------------
/**
@@ -13,3 +16,30 @@ export function createAPIKey(req, res, next) {
.then((key) => res.send(key))
.catch((err) => next(err))
}
+export const generateCoralSSOToken = async (req, res) => {
+ try {
+ if (!req.user) {
+ return res.status(401).json({ message: "Unauthorized" })
+ }
+
+ const payload = {
+ jti: uuidv4(),
+ iat: Math.floor(Date.now() / 1000),
+ exp: Math.floor(Date.now() / 1000) + (60 * 60),
+ user: {
+ id: req.user.id,
+ email: req.user.email,
+ username: req.user.name,
+ },
+ }
+
+ const header = {
+ alg: "HS256",
+ }
+ const token = jwt.sign(payload, config.auth.coral.secret, { header })
+ res.json({ token })
+ } catch (error) {
+ Sentry.captureException(error)
+ res.status(500).json({ message: "Failed to generate Coral SSO token" })
+ }
+}
diff --git a/packages/openneuro-server/src/routes.ts b/packages/openneuro-server/src/routes.ts
index 2e0ae65fc5..865eedffd3 100644
--- a/packages/openneuro-server/src/routes.ts
+++ b/packages/openneuro-server/src/routes.ts
@@ -159,6 +159,13 @@ const routes = [
middleware: [noCache, orcid.authCallback],
handler: jwt.authSuccessHandler,
},
+ // Coral SSO Token Generation ---------------------
+ {
+ method: "get",
+ url: "/auth/coral-sso",
+ middleware: [noCache, jwt.authenticate, auth.authenticated],
+ handler: users.generateCoralSSOToken,
+ },
// GitHub authentication route
{
- Comments -
-