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.
-

- Comments -

-
- Please sign in to contribute to the discussion. -
-
+ 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 {