diff --git a/README.md b/README.md index ed15da8..48198f9 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,15 @@ The easiest way to get started is to use the docker image. This will give you a ```shell -docker-compose up -d +docker compose up -d +docker logs neptyne-spreadsheet-neptyne-1 ``` -Only need to build it once. After that, you can just run `docker-compose up`. +The second statement will print out the shared secret you need to connect to the Neptyne server. +Open that url and you are in business. + +```shell ### Method 2: pip install diff --git a/frontend/src/neptyne-container/NeptyneModals.test.tsx b/frontend/src/neptyne-container/NeptyneModals.test.tsx deleted file mode 100644 index 25da01e..0000000 --- a/frontend/src/neptyne-container/NeptyneModals.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { MutableRefObject, ReactElement, useEffect } from "react"; -import { act, render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { - ModalDispatch, - ModalReducerAction, - ModalReducerShowProps, - NeptyneModals, - useModalDispatch, -} from "./NeptyneModals"; -import noop from "lodash/noop"; -import { NoteDialog } from "../components/ToolbarControls/NoteDialog"; -import { LinkDialog } from "../components/ToolbarControls/LinkDialog"; -import { ShareDialogDataWrapper } from "../ShareDialog/ShareDialogDataWrapper"; -import { - AllowAnonymous, - OpenTyneDialogDataWrapper, -} from "../components/OpenDialog/OpenTyneDialogDataWrapper"; -import { WidgetRegistry } from "../NeptyneProtocol"; -import { AutocompleteResponse } from "../notebook/NotebookCellEditor/types"; -import { User } from "../user-context"; - -const TESTED_MODALS = [ - [OpenTyneDialogDataWrapper, screen.findAllByText.bind(screen, "Test Tyne")], - [ShareDialogDataWrapper, screen.findByText.bind(screen, 'Share "Test Tyne"')], - [NoteDialog, null], - [LinkDialog, null], -] as [ModalReducerShowProps["element"], (() => Promise) | null][]; - -const MOCK_USER = { displayName: "Me", email: "me@meptyne.com" } as User; - -const MOCK_MODAL_PROPS = { - onToggle: noop, - currentCellAttributes: {}, - onCellAttributeChange: noop, - onUpdateCellValues: noop, - widgetRegistry: {} as WidgetRegistry, - sheetSelection: { - start: { - row: 0, - col: 0, - }, - end: { - row: 0, - col: 0, - }, - }, - currentSheet: 0, - getAutocomplete: noop as () => Promise, - getWidgetState: noop as () => Promise<{ [name: string]: string }>, - validateWidgetParams: noop as () => Promise<{ [name: string]: string }>, - onCreateFunctionSubmit: noop, - user: MOCK_USER, - allowAnonymous: "no" as AllowAnonymous, - errorMessage: null, - notificationMessage: null, - onTyneAction: noop, - tyneId: "test_tyne", - tyneName: "Test Tyne", -}; - -jest.mock("../authenticatedFetch", () => { - return async function authenticatedFetch( - _: unknown, - info: Parameters[0] - ) { - const path = info.toString(); - return { - async json() { - if (path === "/api/tyne_list") - return { - tynes: [ - { - access: "EDIT", - categories: ["editableByMe", "authoredByMe"], - fileName: "test", - galleryScreenshotUrl: null, - lastModified: "2022-08-26T16:00:13.433923", - name: "Test Tyne", - owner: "Test User", - ownerColor: "#e8f5a3", - ownerProfileImage: null, - }, - ], - }; - else if (path === `/api/tynes/${MOCK_MODAL_PROPS.tyneId}/share`) - return { - shares: [], - users: [], - published: false, - }; - return {}; - }, - }; - }; -}); - -const ModalContextPortal = React.forwardRef((props, ref) => { - const modalDispatch = useModalDispatch(); - - useEffect(() => { - if (typeof ref === "function") ref(modalDispatch); - else if ("current" in ref!) ref.current = modalDispatch; - }, [ref, modalDispatch]); - - return null; -}); - -describe("NeptyneModals", () => { - describe.each([ - [ - "ref", - (ref: MutableRefObject) => ( - - ), - ], - [ - "context", - (ref: MutableRefObject) => ( - - - - ), - ], - ])("NeptyneModals open via %s", (_, modalContextRenderer) => { - it.each(TESTED_MODALS)("Opens %p", async (Modal, waitFor) => { - const modalDispatch: MutableRefObject = { current: null }; - render( - ( - modalContextRenderer as ( - ref: MutableRefObject - ) => ReactElement - )(modalDispatch) - ); - - expect(typeof modalDispatch.current).toBe("function"); - // Haven't found better generic solution yet - expect(document.querySelector(".MuiModal-root")).toBeNull(); - - await act(async () => { - modalDispatch.current?.({ - action: ModalReducerAction.Show, - props: { - element: Modal as typeof LinkDialog, - }, - }); - }); - await waitFor?.(); - - expect(document.querySelector(".MuiModal-root")).toBeInstanceOf(HTMLElement); - - await act(() => { - modalDispatch.current?.({ - action: ModalReducerAction.Hide, - }); - }); - - expect(document.querySelector(".MuiModal-root")).toBeNull(); - }); - }); - - describe.each([ - [ - "backdrop click", - async () => - await userEvent.click(document.querySelector(".MuiDialog-container")!), - ], - [ - "esc press", - async () => - await userEvent.type(document.querySelector(".MuiDialog-container")!, "{esc}"), - ], - ])("NeptyneModals close via %s", (_, handleClose) => { - it.each(TESTED_MODALS)("Closes %p", async (Modal, waitFor) => { - const modalDispatch: MutableRefObject = { current: null }; - render(); - await act(() => { - modalDispatch.current?.({ - action: ModalReducerAction.Show, - props: { - element: Modal as typeof LinkDialog, - }, - }); - }); - await waitFor?.(); - - expect(document.querySelector(".MuiModal-root")).toBeInstanceOf(HTMLElement); - - await act(async () => { - await (handleClose as () => void)(); - }); - - expect(document.querySelector(".MuiModal-root")).toBeNull(); - }); - }); -}); diff --git a/neptyne_kernel/dash.py b/neptyne_kernel/dash.py index 0c197f5..674ec24 100644 --- a/neptyne_kernel/dash.py +++ b/neptyne_kernel/dash.py @@ -599,6 +599,8 @@ def _discover_google_credentials(self): return self._load_authorized_credentials(authorized_user) elif (oauth_credentials := config_path / "oauth_credentials.json").exists(): return self._load_oauth_credentials(oauth_credentials) + else: + self._google_credentials = None def _authorize_service_account(self, credentials: Path): from google.oauth2.service_account import Credentials diff --git a/neptyne_kernel/neptyne_api/geo.py b/neptyne_kernel/neptyne_api/geo.py index f3044b4..1290b4d 100644 --- a/neptyne_kernel/neptyne_api/geo.py +++ b/neptyne_kernel/neptyne_api/geo.py @@ -99,6 +99,7 @@ def dataset(name_or_url: str) -> GeoDataFrame: path = geopandas.datasets.get_path(name_or_url) else: import geoplot + try: path = geoplot.datasets.get_path(name_or_url) except ValueError: diff --git a/server/kernels/kernel_handlers.py b/server/kernels/kernel_handlers.py index 64deab7..e6db109 100644 --- a/server/kernels/kernel_handlers.py +++ b/server/kernels/kernel_handlers.py @@ -116,7 +116,14 @@ async def init_connection( with self.make_session() as session: user: User | NonUser self.user_profile_image = "" - if auth_token: + if gsheet_auth_token: + assert gsheet_auth_token + user = NonUser.GSHEET + claims = decode_gsheet_extension_token(gsheet_auth_token) + self.user_id = None + self.user_name = f"anon-{claims.sheet_id}" + self.user_email = f"{self.user_name}@example.com" + else: user = await authenticate_request(self, session, token=auth_token) self.user_email = user.email self.user_name = user.name @@ -124,13 +131,6 @@ async def init_connection( self.allow_other_gsheets = bool( self.user_email ) and self.user_email.endswith("@neptyne.com") - else: - assert gsheet_auth_token - user = NonUser.GSHEET - claims = decode_gsheet_extension_token(gsheet_auth_token) - self.user_id = None - self.user_name = f"anon-{claims.sheet_id}" - self.user_email = f"{self.user_name}@example.com" tyne_proxy = await self.tyne_contents_manager.get( self.tyne_id, session, user, gsheet_auth_token diff --git a/server/users.py b/server/users.py index d764e96..780d59c 100644 --- a/server/users.py +++ b/server/users.py @@ -1,10 +1,12 @@ from typing import Any, Literal +from jwt import PyJWTError from sqlalchemy import func from sqlalchemy.orm import Session from tornado import web from tornado_sqlalchemy import SessionMixin +from server.gsheet_auth import decode_gsheet_extension_token from server.models import ( EmailShare, FirebaseUser, @@ -17,9 +19,10 @@ def token_from_headers(request_handler: web.RequestHandler) -> str | None: + header = request_handler.request.headers.get("X-Neptyne-GSheet-Auth-Token") + if header: + return header header = request_handler.request.headers.get("Authorization") - if not header: - return None parts = header.split(" ") if len(parts) != 2 or parts[0].lower() != "bearer": @@ -55,8 +58,11 @@ async def _authenticate_request( ) if not token: raise web.HTTPError(401, "Missing token") - if not token == shared_secret: # TODO: also check for a signed token - raise web.HTTPError(401, "Invalid token") + if not token == shared_secret: + try: + decode_gsheet_extension_token(token) + except PyJWTError: + raise web.HTTPError(401, "Invalid token") return await load_user( session, "",