diff --git a/apps/api/src/auth/base.ts b/apps/api/src/auth/base.ts index 5f431405..687b67d4 100644 --- a/apps/api/src/auth/base.ts +++ b/apps/api/src/auth/base.ts @@ -6,7 +6,7 @@ import { obscureEmail } from '../emails.js' import { comparePassword, hashPassword, isValidPassword } from '../password.js' import properties from '../properties.js' import { IOServer } from '../websocket/index.js' -import { callbackUrlSchema, cookieOptions } from './index.js' +import { cookieOptions } from './index.js' import { authenticationMiddleware, createAuthToken, @@ -137,14 +137,18 @@ export default function getRouter( router.post('/sign-in/password', async (req, res) => { const payload = z - .object({ email: z.string().trim().email(), password: z.string() }) + .object({ + email: z.string().trim().email(), + password: z.string(), + callback: z.string().optional(), + }) .safeParse(req.body) if (!payload.success) { res.status(400).end() return } - const { email, password } = payload.data + const { email, password, callback } = payload.data const user = await prisma().user.findUnique({ where: { email }, @@ -164,7 +168,10 @@ export default function getRouter( return } - const loginLink = createLoginLink(user.id, config.FRONTEND_URL) + const loginLink = createLoginLink( + user.id, + `${config.FRONTEND_URL}/${callback ?? ''}` + ) res.json({ email: obscureEmail(user.email), loginLink }) }) @@ -184,16 +191,9 @@ export default function getRouter( }) }) - router.get('/logout', authenticationMiddleware, async (req, res) => { - const query = z.object({ callback: callbackUrlSchema }).safeParse(req.query) - if (!query.success) { - res.status(400).end() - return - } - + router.get('/logout', async (_req, res) => { res.clearCookie('token') - - res.redirect(query.data.callback) + res.redirect(config.FRONTEND_URL) }) return router diff --git a/apps/api/src/auth/index.ts b/apps/api/src/auth/index.ts index ce9a2f31..012b93e3 100644 --- a/apps/api/src/auth/index.ts +++ b/apps/api/src/auth/index.ts @@ -1,4 +1,3 @@ -import { z } from 'zod' import { CookieOptions } from 'express' import { config } from '../config/index.js' import parseDuration from 'parse-duration' @@ -7,12 +6,6 @@ import getBaseRouter from './base.js' const JWT_EXPIRATION_MS = parseDuration(config().AUTH_JWT_EXPIRATION) -export const callbackUrlSchema = z - .string() - .trim() - .url() - .refine((url) => new URL(url).origin === config().FRONTEND_URL) - export const cookieOptions: CookieOptions = { httpOnly: true, secure: config().NODE_ENV === 'production' && !config().ALLOW_HTTP, diff --git a/apps/api/src/auth/token.ts b/apps/api/src/auth/token.ts index 5490071d..1dde2783 100644 --- a/apps/api/src/auth/token.ts +++ b/apps/api/src/auth/token.ts @@ -147,7 +147,7 @@ export async function authenticationMiddleware( try { const session = await sessionFromCookies(req.cookies) if (!session) { - res.status(401).end() + res.status(401).json('Unauthorized') return } @@ -156,12 +156,12 @@ export async function authenticationMiddleware( next() } catch (err) { if (err instanceof jwt.JsonWebTokenError) { - res.status(403).send('Invalid token') + res.status(403).json('Forbidden') return } req.log.error({ err }, 'Error verifying token') - res.status(500).end() + res.status(500).json('Internal server error') } } diff --git a/apps/api/src/websocket/workspace/documents.ts b/apps/api/src/websocket/workspace/documents.ts index 31aca87f..b29eedaa 100644 --- a/apps/api/src/websocket/workspace/documents.ts +++ b/apps/api/src/websocket/workspace/documents.ts @@ -1,8 +1,15 @@ -import { getDocument, listDocuments } from '@briefer/database' +import { createDocument, getDocument, listDocuments } from '@briefer/database' import { IOServer, Socket } from '../index.js' export async function emitDocuments(socket: Socket, workspaceId: string) { - const documents = await listDocuments(workspaceId) + let documents = await listDocuments(workspaceId) + if (documents.length === 0) { + await createDocument(workspaceId, { + orderIndex: 0, + version: 2, + }) + documents = await listDocuments(workspaceId) + } socket.emit('workspace-documents', { workspaceId, documents }) } diff --git a/apps/web/src/components/BannedPage.tsx b/apps/web/src/components/BannedPage.tsx deleted file mode 100644 index 73c89ebd..00000000 --- a/apps/web/src/components/BannedPage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const BannedPage = () => { - return ( -
-

Account inactive. Please contact us.

-
- ) -} - -export default BannedPage diff --git a/apps/web/src/components/Comments.tsx b/apps/web/src/components/Comments.tsx index d699a4f6..6da90f8b 100644 --- a/apps/web/src/components/Comments.tsx +++ b/apps/web/src/components/Comments.tsx @@ -20,7 +20,7 @@ interface Props { onHide: () => void } export default function Comments(props: Props) { - const session = useSession() + const session = useSession({ redirectToLogin: true }) const [comments, { createComment }] = useComments(props.documentId) const [content, setContent] = useState('') diff --git a/apps/web/src/components/Dashboard/index.tsx b/apps/web/src/components/Dashboard/index.tsx index 4240e3ee..fab31c50 100644 --- a/apps/web/src/components/Dashboard/index.tsx +++ b/apps/web/src/components/Dashboard/index.tsx @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { SquaresPlusIcon } from '@heroicons/react/24/solid' import { BookUpIcon } from 'lucide-react' import { EyeIcon } from '@heroicons/react/24/outline' @@ -28,12 +28,12 @@ import Files from '../Files' import { PublishBlinkingSignal } from '../BlinkingSignal' import { Tooltip } from '../Tooltips' import { NEXT_PUBLIC_PUBLIC_URL } from '@/utils/env' -import useWebsocket from '@/hooks/useWebsocket' import { SQLExtensionProvider } from '../v2Editor/CodeEditor/sql' +import { SessionUser } from '@/hooks/useAuth' interface Props { document: ApiDocument - userId: string + user: SessionUser role: UserWorkspaceRole isEditing: boolean publish: () => Promise @@ -45,12 +45,12 @@ export default function Dashboard(props: Props) { return props.document.clock } - return props.document.userAppClock[props.userId] ?? props.document.appClock + return props.document.userAppClock[props.user.id] ?? props.document.appClock }, [ props.isEditing, props.document.clock, props.document.userAppClock, - props.userId, + props.user, ]) const { yDoc, syncing, isDirty } = useYDoc( @@ -58,7 +58,7 @@ export default function Dashboard(props: Props) { props.document.id, !props.isEditing, clock, - props.userId, + props.user.id, props.document.publishedAt, true, null @@ -74,7 +74,6 @@ export default function Dashboard(props: Props) { const aiTasks = useMemo(() => AITasks.fromYjs(yDoc), [yDoc]) const router = useRouter() - const socket = useWebsocket() const onPublish = useCallback(async () => { if (props.publishing) { @@ -232,6 +231,7 @@ export default function Dashboard(props: Props) {
@@ -264,7 +264,7 @@ export default function Dashboard(props: Props) { disabled={false} yDoc={yDoc} primary={true} - userId={props.userId} + userId={props.user.id} executionQueue={executionQueue} /> )} @@ -296,7 +296,7 @@ export default function Dashboard(props: Props) { workspaceId={props.document.workspaceId} visible={selectedSidebar === 'files'} onHide={onHideSidebar} - userId={props.userId} + userId={props.user.id} yDoc={yDoc} executionQueue={executionQueue} /> @@ -348,7 +348,7 @@ function DashboardContent( latestBlockId={latestBlockId} isEditing={props.isEditing} userRole={props.role} - userId={props.userId} + userId={props.user.id} executionQueue={props.executionQueue} aiTasks={props.aiTasks} /> @@ -359,7 +359,7 @@ function DashboardContent( yDoc={props.yDoc} onDragStart={onDragStart} onAddBlock={onAddBlock} - userId={props.userId} + userId={props.user.id} executionQueue={props.executionQueue} aiTasks={props.aiTasks} /> diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx index 9f1eab56..6b9528ef 100644 --- a/apps/web/src/components/Layout.tsx +++ b/apps/web/src/components/Layout.tsx @@ -30,17 +30,14 @@ import { useRouter } from 'next/router' import { Page } from '@/components/PagePath' import { useDocuments } from '@/hooks/useDocuments' import { useFavorites } from '@/hooks/useFavorites' -import { useWorkspaces } from '@/hooks/useWorkspaces' import { useStringQuery } from '@/hooks/useQueryArgs' -import { useSession, useSignout } from '@/hooks/useAuth' +import { SessionUser, useSession, useSignout } from '@/hooks/useAuth' import { CpuChipIcon } from '@heroicons/react/24/solid' import type { UserWorkspaceRole } from '@briefer/database' import ReactDOM from 'react-dom' import useDropdownPosition from '@/hooks/dropdownPosition' import DocumentTree from './DocumentsTree' import useSideBar from '@/hooks/useSideBar' -import { isBanned } from '@/utils/isBanned' -import BannedPage from './BannedPage' import { SubscriptionBadge } from './SubscriptionBadge' import MobileWarning from './MobileWarning' import { DataSourceBlinkingSignal } from './BlinkingSignal' @@ -109,6 +106,7 @@ interface Props { topBarClassname?: string topBarContent?: React.ReactNode hideOnboarding?: boolean + user: SessionUser } export default function Layout({ @@ -116,10 +114,9 @@ export default function Layout({ pagePath, topBarClassname, topBarContent, + user, hideOnboarding, }: Props) { - const session = useSession() - const [isSearchOpen, setSearchOpen] = useState(false) useHotkeys(['mod+k'], () => { setSearchOpen((prev) => !prev) @@ -137,21 +134,6 @@ export default function Layout({ const workspaceId = useStringQuery('workspaceId') const documentId = useStringQuery('documentId') - const [{ data: workspaces, isLoading: isLoadingWorkspaces }] = useWorkspaces() - - const signOut = useSignout() - useEffect(() => { - const workspace = workspaces.find((w) => w.id === workspaceId) - - if (!workspace && !isLoadingWorkspaces) { - if (workspaces.length > 0) { - router.replace(`/workspaces/${workspaces[0].id}/documents`) - } else { - signOut() - } - } - }, [workspaces, isLoadingWorkspaces, signOut]) - const [ documentsState, { @@ -270,14 +252,14 @@ export default function Layout({ return false } - const role = session.data?.roles[workspaceId] + const role = user.roles[workspaceId] if (!role) { return false } return item.allowedRoles.has(role) }, - [session.data, workspaceId] + [user, workspaceId] ) const scrollRef = useRef(null) @@ -303,11 +285,6 @@ export default function Layout({ } }, [workspaceId, scrollRef]) - const userEmail = session.data?.email - if (userEmail && isBanned(userEmail)) { - return - } - return (
@@ -348,7 +325,7 @@ export default function Layout({ onFavorite={onFavoriteDocument} onUnfavorite={onUnfavoriteDocument} onSetIcon={onSetIcon} - role={session.data?.roles[workspaceId] ?? 'viewer'} + role={user.roles[workspaceId] ?? 'viewer'} flat={true} onCreate={onCreateDocument} onUpdateParent={onUpdateDocumentParent} @@ -376,7 +353,7 @@ export default function Layout({ /> - {session.data?.roles[workspaceId] !== 'viewer' && ( + {user.roles[workspaceId] !== 'viewer' && (