feat: guest auth (#141)

* feat: Start using guest auth

* feat: Implement more functionality to work as guest

* chore: update package and tweak public route

* chore: update root package json

* chore: update web package json

---------

Co-authored-by: Aslam H <iupin5212@gmail.com>
This commit is contained in:
Anselm Eickhoff
2024-09-06 21:11:43 +01:00
committed by GitHub
parent e61aae02d5
commit 844b1ae334
16 changed files with 138 additions and 205 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -15,7 +15,7 @@
"dependencies": { "dependencies": {
"@tauri-apps/cli": "^2.0.0-rc.12", "@tauri-apps/cli": "^2.0.0-rc.12",
"@tauri-apps/plugin-fs": "^2.0.0-rc.2", "@tauri-apps/plugin-fs": "^2.0.0-rc.2",
"jazz-nodejs": "0.7.35-unique.2", "jazz-nodejs": "0.7.35-guest-auth.5",
"react-icons": "^5.3.0" "react-icons": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,32 +1,30 @@
import { SignedInClient } from "@/components/custom/clerk/signed-in-client" "use client"
import { Sidebar } from "@/components/custom/sidebar/sidebar" import { Sidebar } from "@/components/custom/sidebar/sidebar"
import { JazzClerkAuth, JazzProvider } from "@/lib/providers/jazz-provider"
import { currentUser } from "@clerk/nextjs/server"
import { CommandPalette } from "@/components/custom/command-palette/command-palette" import { CommandPalette } from "@/components/custom/command-palette/command-palette"
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { usePathname } from "next/navigation"
import PublicHomeRoute from "@/components/routes/public/PublicHomeRoute"
export default async function PageLayout({ children }: { children: React.ReactNode }) { export default function PageLayout({ children }: { children: React.ReactNode }) {
const user = await currentUser() const { me } = useAccountOrGuest()
const pathname = usePathname()
if (!user) { if (me._type === "Anonymous" && pathname === "/") {
return children return <PublicHomeRoute />
} }
return ( return (
<JazzClerkAuth> <div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
<SignedInClient> <Sidebar />
<JazzProvider>
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
<Sidebar />
<CommandPalette />
<div className="flex min-w-0 flex-1 flex-col"> {me._type !== "Anonymous" && <CommandPalette />}
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
{children} <div className="flex min-w-0 flex-1 flex-col">
</main> <main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
</div> {children}
</div> </main>
</JazzProvider> </div>
</SignedInClient> </div>
</JazzClerkAuth>
) )
} }

View File

@@ -8,6 +8,7 @@ import { Toaster } from "@/components/ui/sonner"
import { ConfirmProvider } from "@/lib/providers/confirm-provider" import { ConfirmProvider } from "@/lib/providers/confirm-provider"
import { DeepLinkProvider } from "@/lib/providers/deep-link-provider" import { DeepLinkProvider } from "@/lib/providers/deep-link-provider"
import { GeistMono, GeistSans } from "./fonts" import { GeistMono, GeistSans } from "./fonts"
import { JazzAndAuth } from "@/lib/providers/jazz-provider"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Learn Anything", title: "Learn Anything",
@@ -27,7 +28,7 @@ export default function RootLayout({
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<JotaiProvider> <JotaiProvider>
<ConfirmProvider> <ConfirmProvider>
{children} <JazzAndAuth>{children}</JazzAndAuth>
<Toaster expand={false} /> <Toaster expand={false} />
</ConfirmProvider> </ConfirmProvider>
</JotaiProvider> </JotaiProvider>

View File

@@ -14,6 +14,8 @@ import { LinkSection } from "./partial/link-section"
import { PageSection } from "./partial/page-section" import { PageSection } from "./partial/page-section"
import { TopicSection } from "./partial/topic-section" import { TopicSection } from "./partial/topic-section"
import { ProfileSection } from "./partial/profile-section" import { ProfileSection } from "./partial/profile-section"
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { SignInButton } from "@clerk/nextjs"
interface SidebarContextType { interface SidebarContextType {
isCollapsed: boolean isCollapsed: boolean
@@ -109,7 +111,9 @@ const LogoAndSearch: React.FC = React.memo(() => {
LogoAndSearch.displayName = "LogoAndSearch" LogoAndSearch.displayName = "LogoAndSearch"
const SidebarContent: React.FC = React.memo(() => { const SidebarContent: React.FC = React.memo(() => {
const { me } = useAccountOrGuest()
const pathname = usePathname() const pathname = usePathname()
return ( return (
<> <>
<nav className="bg-background relative flex h-full w-full shrink-0 flex-col"> <nav className="bg-background relative flex h-full w-full shrink-0 flex-col">
@@ -118,12 +122,19 @@ const SidebarContent: React.FC = React.memo(() => {
</div> </div>
<div tabIndex={-1} className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3"> <div tabIndex={-1} className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3">
<div className="h-2 shrink-0" /> <div className="h-2 shrink-0" />
<LinkSection pathname={pathname} /> {me._type === "Account" && <LinkSection pathname={pathname} />}
<PageSection pathname={pathname} /> {me._type === "Account" && <PageSection pathname={pathname} />}
<TopicSection pathname={pathname} /> {me._type === "Account" && <TopicSection pathname={pathname} />}
</div> </div>
</nav> </nav>
<ProfileSection />
{me._type === "Account" ? (
<ProfileSection />
) : (
<div className="visible absolute inset-x-0 bottom-0 z-10 flex gap-8 p-2.5">
<SignInButton>Fake profile section</SignInButton>
</div>
)}
</> </>
) )
}) })

View File

@@ -13,7 +13,7 @@ interface GraphNode {
interface AutocompleteProps { interface AutocompleteProps {
topics: GraphNode[] topics: GraphNode[]
onSelect: (topic: GraphNode) => void onSelect: (topic: string) => void
onInputChange: (value: string) => void onInputChange: (value: string) => void
} }
@@ -46,18 +46,16 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
const handleSelect = useCallback( const handleSelect = useCallback(
(topic: GraphNode) => { (topic: GraphNode) => {
setInputValue(topic.prettyName) // setInputValue(topicPrettyName)
setOpen(false) setOpen(false)
onSelect(topic) onSelect(topic.name)
}, },
[onSelect] [onSelect]
) )
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => { (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" && filteredTopics.length > 0) { if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") {
handleSelect(filteredTopics[0])
} else if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") {
setOpen(true) setOpen(true)
setIsInitialOpen(true) setIsInitialOpen(true)
} }
@@ -65,7 +63,7 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
setHasInteracted(true) setHasInteracted(true)
} }
}, },
[filteredTopics, handleSelect, hasInteracted] [hasInteracted]
) )
const handleInputChange = useCallback( const handleInputChange = useCallback(
@@ -143,6 +141,7 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
{filteredTopics.map((topic, index) => ( {filteredTopics.map((topic, index) => (
<CommandItem <CommandItem
key={index} key={index}
value={topic.name}
onSelect={() => handleSelect(topic)} onSelect={() => handleSelect(topic)}
className="min-h-10 rounded-none px-3 py-1.5" className="min-h-10 rounded-none px-3 py-1.5"
> >

View File

@@ -23,8 +23,8 @@ export function PublicHomeRoute() {
const raw_graph_data = React.use(graph_data_promise) as GraphNode[] const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
const [filterQuery, setFilterQuery] = React.useState<string>("") const [filterQuery, setFilterQuery] = React.useState<string>("")
const handleTopicSelect = (topicName: string) => { const handleTopicSelect = (topic: string) => {
router.push(`/${topicName}`) router.replace(`/${topic}`)
} }
const handleInputChange = (value: string) => { const handleInputChange = (value: string) => {
@@ -34,11 +34,7 @@ export function PublicHomeRoute() {
return ( return (
<> <>
<div className="relative h-full w-screen"> <div className="relative h-full w-screen">
<ForceGraphClient <ForceGraphClient raw_nodes={raw_graph_data} onNodeClick={handleTopicSelect} filter_query={filterQuery} />
raw_nodes={raw_graph_data}
onNodeClick={val => handleTopicSelect(val)}
filter_query={filterQuery}
/>
<div className="absolute left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 transform max-sm:px-5"> <div className="absolute left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 transform max-sm:px-5">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
@@ -53,11 +49,7 @@ export function PublicHomeRoute() {
> >
I want to learn I want to learn
</motion.h1> </motion.h1>
<Autocomplete <Autocomplete topics={raw_graph_data} onSelect={handleTopicSelect} onInputChange={handleInputChange} />
topics={raw_graph_data}
onSelect={topic => handleTopicSelect(topic.name)}
onInputChange={handleInputChange}
/>
</motion.div> </motion.div>
</div> </div>
</div> </div>

View File

@@ -4,15 +4,16 @@ import * as React from "react"
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header" import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
import { ListOfTopics, Topic } from "@/lib/schema" import { ListOfTopics, Topic } from "@/lib/schema"
import { LearningStateSelector } from "@/components/custom/learning-state-selector" import { LearningStateSelector } from "@/components/custom/learning-state-selector"
import { useAccount } from "@/lib/providers/jazz-provider" import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { LearningStateValue } from "@/lib/constants" import { LearningStateValue } from "@/lib/constants"
import { toast } from "sonner"
interface TopicDetailHeaderProps { interface TopicDetailHeaderProps {
topic: Topic topic: Topic
} }
export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }: TopicDetailHeaderProps) { export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }: TopicDetailHeaderProps) {
const { me } = useAccount({ const { me } = useAccountOrGuest({
root: { root: {
topicsWantToLearn: [], topicsWantToLearn: [],
topicsLearning: [], topicsLearning: [],
@@ -26,34 +27,44 @@ export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }
learningState: LearningStateValue learningState: LearningStateValue
} | null = null } | null = null
const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1 const wantToLearnIndex =
me?._type === "Anonymous" ? -1 : (me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1)
if (wantToLearnIndex !== -1) { if (wantToLearnIndex !== -1) {
p = { p = {
index: wantToLearnIndex, index: wantToLearnIndex,
topic: me?.root.topicsWantToLearn[wantToLearnIndex], // TODO: fix this type error by doing better conditionals on both index and p
topic: me && me._type !== "Anonymous" ? me.root.topicsWantToLearn[wantToLearnIndex] : undefined,
learningState: "wantToLearn" learningState: "wantToLearn"
} }
} }
const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1 const learningIndex =
me?._type === "Anonymous" ? -1 : (me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1)
if (learningIndex !== -1) { if (learningIndex !== -1) {
p = { p = {
index: learningIndex, index: learningIndex,
topic: me?.root.topicsLearning[learningIndex], topic: me && me._type !== "Anonymous" ? me?.root.topicsLearning[learningIndex] : undefined,
learningState: "learning" learningState: "learning"
} }
} }
const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1 const learnedIndex =
me?._type === "Anonymous" ? -1 : (me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1)
if (learnedIndex !== -1) { if (learnedIndex !== -1) {
p = { p = {
index: learnedIndex, index: learnedIndex,
topic: me?.root.topicsLearned[learnedIndex], topic: me && me._type !== "Anonymous" ? me?.root.topicsLearned[learnedIndex] : undefined,
learningState: "learned" learningState: "learned"
} }
} }
const handleAddToProfile = (learningState: LearningStateValue) => { const handleAddToProfile = (learningState: LearningStateValue) => {
if (me?._type === "Anonymous") {
// TODO: handle better
toast.error("You need to sign in to add links to your personal list.")
return
}
const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = { const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = {
wantToLearn: me?.root.topicsWantToLearn, wantToLearn: me?.root.topicsWantToLearn,
learning: me?.root.topicsLearning, learning: me?.root.topicsLearning,

View File

@@ -4,9 +4,8 @@ import React, { useMemo, useRef } from "react"
import { TopicDetailHeader } from "./Header" import { TopicDetailHeader } from "./Header"
import { TopicSections } from "./partials/topic-sections" import { TopicSections } from "./partials/topic-sections"
import { atom } from "jotai" import { atom } from "jotai"
import { useAccount, useCoState } from "@/lib/providers/jazz-provider" import { useAccount, useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { Topic } from "@/lib/schema" import { useTopicData } from "@/hooks/use-topic-data"
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
interface TopicDetailRouteProps { interface TopicDetailRouteProps {
topicName: string topicName: string
@@ -15,10 +14,8 @@ interface TopicDetailRouteProps {
export const openPopoverForIdAtom = atom<string | null>(null) export const openPopoverForIdAtom = atom<string | null>(null)
export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) { export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
const { me } = useAccount({ root: { personalLinks: [] } }) const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
const { topic } = useTopicData(topicName, me)
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me), [topicName, me])
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [{ links: [] }] } })
// const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks) // const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks)
const linksRefDummy = useRef<(HTMLLIElement | null)[]>([]) const linksRefDummy = useRef<(HTMLLIElement | null)[]>([])
const containerRefDummy = useRef<HTMLDivElement>(null) const containerRefDummy = useRef<HTMLDivElement>(null)
@@ -37,8 +34,6 @@ export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
setActiveIndex={() => {}} setActiveIndex={() => {}}
linkRefs={linksRefDummy} linkRefs={linksRefDummy}
containerRef={containerRefDummy} containerRef={containerRefDummy}
me={me}
personalLinks={me.root.personalLinks}
/> />
</div> </div>
) )

View File

@@ -13,6 +13,7 @@ import { cn, ensureUrlProtocol, generateUniqueSlug } from "@/lib/utils"
import { LaAccount, Link as LinkSchema, PersonalLink, PersonalLinkLists, Topic, UserRoot } from "@/lib/schema" import { LaAccount, Link as LinkSchema, PersonalLink, PersonalLinkLists, Topic, UserRoot } from "@/lib/schema"
import { openPopoverForIdAtom } from "../TopicDetailRoute" import { openPopoverForIdAtom } from "../TopicDetailRoute"
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
interface LinkItemProps { interface LinkItemProps {
topic: Topic topic: Topic
@@ -20,23 +21,24 @@ interface LinkItemProps {
isActive: boolean isActive: boolean
index: number index: number
setActiveIndex: (index: number) => void setActiveIndex: (index: number) => void
me: {
root: {
personalLinks: PersonalLinkLists
} & UserRoot
} & LaAccount
personalLinks: PersonalLinkLists
} }
export const LinkItem = React.memo( export const LinkItem = React.memo(
React.forwardRef<HTMLLIElement, LinkItemProps>( React.forwardRef<HTMLLIElement, LinkItemProps>(
({ topic, link, isActive, index, setActiveIndex, me, personalLinks }, ref) => { ({ topic, link, isActive, index, setActiveIndex }, ref) => {
const router = useRouter() const router = useRouter()
const [, setOpenPopoverForId] = useAtom(openPopoverForIdAtom) const [, setOpenPopoverForId] = useAtom(openPopoverForIdAtom)
const [isPopoverOpen, setIsPopoverOpen] = useState(false) const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { me } = useAccountOrGuest({ root: { personalLinks: [] } });
const personalLinks = useMemo(() => {
if (!me || me._type === "Anonymous") return undefined;
return me?.root?.personalLinks || []
}, [me])
const personalLink = useMemo(() => { const personalLink = useMemo(() => {
return personalLinks.find(pl => pl?.link?.id === link.id) return personalLinks?.find(pl => pl?.link?.id === link.id)
}, [personalLinks, link.id]) }, [personalLinks, link.id])
const selectedLearningState = useMemo(() => { const selectedLearningState = useMemo(() => {
@@ -53,6 +55,14 @@ export const LinkItem = React.memo(
const handleSelectLearningState = useCallback( const handleSelectLearningState = useCallback(
(learningState: LearningStateValue) => { (learningState: LearningStateValue) => {
if (!personalLinks || !me || me?._type === "Anonymous") {
if (me?._type === "Anonymous") {
// TODO: handle better
toast.error("You need to sign in to add links to your personal list.")
}
return
};
const defaultToast = { const defaultToast = {
duration: 5000, duration: 5000,
position: "bottom-right" as const, position: "bottom-right" as const,

View File

@@ -11,24 +11,9 @@ interface SectionProps {
startIndex: number startIndex: number
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]> linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
setActiveIndex: (index: number) => void setActiveIndex: (index: number) => void
me: {
root: {
personalLinks: PersonalLinkLists
} & UserRoot
} & LaAccount
personalLinks: PersonalLinkLists
} }
export function Section({ export function Section({ topic, section, activeIndex, setActiveIndex, startIndex, linkRefs }: SectionProps) {
topic,
section,
activeIndex,
setActiveIndex,
startIndex,
linkRefs,
me,
personalLinks
}: SectionProps) {
const [nLinksToLoad, setNLinksToLoad] = useState(10) const [nLinksToLoad, setNLinksToLoad] = useState(10)
const linksToLoad = useMemo(() => { const linksToLoad = useMemo(() => {
@@ -55,8 +40,6 @@ export function Section({
ref={el => { ref={el => {
linkRefs.current[startIndex + index] = el linkRefs.current[startIndex + index] = el
}} }}
me={me}
personalLinks={personalLinks}
/> />
) : ( ) : (
<Skeleton key={index} className="h-14 w-full xl:h-11" /> <Skeleton key={index} className="h-14 w-full xl:h-11" />

View File

@@ -9,12 +9,6 @@ interface TopicSectionsProps {
setActiveIndex: (index: number) => void setActiveIndex: (index: number) => void
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]> linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
containerRef: React.RefObject<HTMLDivElement> containerRef: React.RefObject<HTMLDivElement>
me: {
root: {
personalLinks: PersonalLinkLists
} & UserRoot
} & LaAccount
personalLinks: PersonalLinkLists
} }
export function TopicSections({ export function TopicSections({
@@ -24,8 +18,6 @@ export function TopicSections({
setActiveIndex, setActiveIndex,
linkRefs, linkRefs,
containerRef, containerRef,
me,
personalLinks
}: TopicSectionsProps) { }: TopicSectionsProps) {
return ( return (
<div ref={containerRef} className="flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]"> <div ref={containerRef} className="flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]">
@@ -42,8 +34,6 @@ export function TopicSections({
setActiveIndex={setActiveIndex} setActiveIndex={setActiveIndex}
startIndex={sections.slice(0, sectionIndex).reduce((acc, s) => acc + (s?.links?.length || 0), 0)} startIndex={sections.slice(0, sectionIndex).reduce((acc, s) => acc + (s?.links?.length || 0), 0)}
linkRefs={linkRefs} linkRefs={linkRefs}
me={me}
personalLinks={personalLinks}
/> />
) )
)} )}

View File

@@ -0,0 +1,15 @@
import { useMemo } from "react"
import { useCoState } from "@/lib/providers/jazz-provider"
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
import { Account, AnonymousJazzAgent, ID } from "jazz-tools"
import { Link, Topic } from "@/lib/schema"
const GLOBAL_GROUP_ID = process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>
export function useTopicData(topicName: string, me: Account | AnonymousJazzAgent | undefined) {
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, GLOBAL_GROUP_ID, me), [topicName, me])
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [{ links: [] }] } })
return { topic }
}

View File

@@ -3,104 +3,27 @@
import { createJazzReactApp } from "jazz-react" import { createJazzReactApp } from "jazz-react"
import { LaAccount } from "@/lib/schema" import { LaAccount } from "@/lib/schema"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { createContext, useMemo, useState } from "react" import { useJazzClerkAuth } from "jazz-react-auth-clerk"
import { AuthMethodCtx } from "jazz-react"
const Jazz = createJazzReactApp({ const Jazz = createJazzReactApp({
AccountSchema: LaAccount AccountSchema: LaAccount
}) })
export const { useAccount, useCoState, useAcceptInvite } = Jazz export const { useAccount, useAccountOrGuest, useCoState, useAcceptInvite } = Jazz
export function JazzProvider({ children }: { children: React.ReactNode }) { export function JazzAndAuth({ children }: { children: React.ReactNode }) {
return <Jazz.Provider peer="wss://mesh.jazz.tools/?key=example@gmail.com">{children}</Jazz.Provider>
}
export const JazzClerkAuthCtx = createContext<{
errors: string[]
}>({
errors: []
})
export function JazzClerkAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk() const clerk = useClerk()
const [errors, setErrors] = useState<string[]>([])
const authMethod = useMemo(() => { const [auth, state] = useJazzClerkAuth(clerk)
return new BrowserClerkAuth(
{
onError: error => {
void clerk.signOut()
setErrors(errors => [...errors, error.toString()])
}
},
clerk
)
}, [clerk])
return ( return (
<JazzClerkAuthCtx.Provider value={{ errors }}> <>
<AuthMethodCtx.Provider value={authMethod}>{children}</AuthMethodCtx.Provider> {state.errors.map((error) => (
</JazzClerkAuthCtx.Provider> <div key={error}>{error}</div>
))}
<Jazz.Provider auth={auth || "guest"} peer="wss://mesh.jazz.tools/?key=example@gmail.com">
{children}
</Jazz.Provider>
</>
) )
} }
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools"
import type { LoadedClerk } from "@clerk/types"
import { AgentSecret } from "cojson"
export class BrowserClerkAuth implements AuthMethod {
constructor(
public driver: BrowserClerkAuth.Driver,
private readonly clerkClient: LoadedClerk
) {}
async start(): Promise<AuthResult> {
if (this.clerkClient.user) {
const storedCredentials = this.clerkClient.user.unsafeMetadata
if (storedCredentials.jazzAccountID) {
if (!storedCredentials.jazzAccountSecret) {
throw new Error("No secret for existing user")
}
return {
type: "existing",
credentials: {
accountID: storedCredentials.jazzAccountID as ID<Account>,
secret: storedCredentials.jazzAccountSecret as AgentSecret
},
onSuccess: () => {},
onError: (error: string | Error) => {
this.driver.onError(error)
}
}
} else {
return {
type: "new",
creationProps: {
name: this.clerkClient.user.fullName || this.clerkClient.user.username || this.clerkClient.user.id
},
saveCredentials: async (credentials: { accountID: ID<Account>; secret: AgentSecret }) => {
await this.clerkClient.user?.update({
unsafeMetadata: {
jazzAccountID: credentials.accountID,
jazzAccountSecret: credentials.secret
}
})
},
onSuccess: () => {},
onError: (error: string | Error) => {
this.driver.onError(error)
}
}
}
} else {
throw new Error("Not signed in")
}
}
}
export namespace BrowserClerkAuth {
export interface Driver {
onError: (error: string | Error) => void
}
}

View File

@@ -1,19 +1,23 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const publicRoutes = ["/", "/sign-in(.*)", "/sign-up(.*)"] const isPublicRoute = createRouteMatcher([
const isPublicRoute = createRouteMatcher(publicRoutes) '/sign-in(.*)',
'/sign-up(.*)',
'/',
'/:topicName(.*)'
])
export default clerkMiddleware((auth, request) => { export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) { if (!isPublicRoute(request)) {
auth().protect() auth().protect()
} }
}) })
export const config = { export const config = {
matcher: [ matcher: [
// Skip Next.js internals and all static files, unless found in search params // Skip Next.js internals and all static files, unless found in search params
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes // Always run for API routes
"/(api|trpc)(.*)" '/(api|trpc)(.*)'
] ]
} }

View File

@@ -70,9 +70,10 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"framer-motion": "^11.5.4", "framer-motion": "^11.5.4",
"geist": "^1.3.1", "geist": "^1.3.1",
"jazz-react": "0.7.35-unique.2", "jazz-react": "0.7.35-guest-auth.5",
"jazz-react-auth-clerk": "0.7.33-new-auth.1", "jazz-browser-auth-clerk": "0.7.35-guest-auth.5",
"jazz-tools": "0.7.35-unique.2", "jazz-react-auth-clerk": "0.7.35-guest-auth.5",
"jazz-tools": "0.7.35-guest-auth.5",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"lowlight": "^3.1.0", "lowlight": "^3.1.0",
"lucide-react": "^0.429.0", "lucide-react": "^0.429.0",