From f7bc5fd9582510cfa1e456dae8994eaefd4a4eff Mon Sep 17 00:00:00 2001 From: kenneth Date: Sun, 5 Oct 2025 22:14:44 +0000 Subject: [PATCH] fix: root directory creation Co-authored-by: Ona --- packages/convex/_generated/api.d.ts | 4 +- packages/convex/auth.ts | 26 +++++++------ packages/convex/functions.ts | 7 ++-- packages/convex/model/filesystem.ts | 24 ++++++++++++ packages/convex/model/user.ts | 39 +++---------------- packages/convex/schema.ts | 7 +--- packages/convex/user.ts | 8 ++++ packages/convex/users.ts | 32 --------------- packages/web/src/auth-client.ts | 12 ------ packages/web/src/auth.ts | 27 +++++++++++++ packages/web/src/routes/__root.tsx | 2 +- packages/web/src/routes/_authenticated.tsx | 9 ++++- .../routes/_authenticated/_sidebar-layout.tsx | 1 - packages/web/src/routes/sign-up.tsx | 4 +- 14 files changed, 95 insertions(+), 107 deletions(-) create mode 100644 packages/convex/user.ts delete mode 100644 packages/convex/users.ts delete mode 100644 packages/web/src/auth-client.ts create mode 100644 packages/web/src/auth.ts diff --git a/packages/convex/_generated/api.d.ts b/packages/convex/_generated/api.d.ts index df581dc..b0ba55d 100644 --- a/packages/convex/_generated/api.d.ts +++ b/packages/convex/_generated/api.d.ts @@ -18,7 +18,7 @@ import type * as model_error from "../model/error.js"; import type * as model_files from "../model/files.js"; import type * as model_filesystem from "../model/filesystem.js"; import type * as model_user from "../model/user.js"; -import type * as users from "../users.js"; +import type * as user from "../user.js"; import type { ApiFromModules, @@ -45,7 +45,7 @@ declare const fullApi: ApiFromModules<{ "model/files": typeof model_files; "model/filesystem": typeof model_filesystem; "model/user": typeof model_user; - users: typeof users; + user: typeof user; }>; declare const fullApiWithMounts: typeof fullApi; diff --git a/packages/convex/auth.ts b/packages/convex/auth.ts index f07ac92..a8d1fe6 100644 --- a/packages/convex/auth.ts +++ b/packages/convex/auth.ts @@ -3,13 +3,26 @@ import { convex, crossDomain } from "@convex-dev/better-auth/plugins" import { betterAuth } from "better-auth" import { components } from "./_generated/api" import type { DataModel } from "./_generated/dataModel" -import { query } from "./_generated/server" const siteUrl = process.env.SITE_URL! // The component client has methods needed for integrating Convex with Better Auth, // as well as helper methods for general use. -export const authComponent = createClient(components.betterAuth) +export const authComponent = createClient(components.betterAuth, { + triggers: { + user: { + onCreate: async (ctx, user) => { + const now = Date.now() + await ctx.db.insert("directories", { + name: "", + userId: user._id, + createdAt: now, + updatedAt: now, + }) + }, + }, + }, +}) export const createAuth = ( ctx: GenericCtx, @@ -36,12 +49,3 @@ export const createAuth = ( ], }) } - -// Example function for getting the current user -// Feel free to edit, omit, etc. -export const getCurrentUser = query({ - args: {}, - handler: async (ctx) => { - return authComponent.getAuthUser(ctx) - }, -}) diff --git a/packages/convex/functions.ts b/packages/convex/functions.ts index a5e45e8..00cfcf3 100644 --- a/packages/convex/functions.ts +++ b/packages/convex/functions.ts @@ -4,18 +4,17 @@ import { customMutation, customQuery, } from "convex-helpers/server/customFunctions" -import type { Doc } from "./_generated/dataModel" import type { MutationCtx, QueryCtx } from "./_generated/server" import { mutation, query } from "./_generated/server" -import { userIdentityOrThrow, userOrThrow } from "./model/user" +import { type AuthUser, userIdentityOrThrow, userOrThrow } from "./model/user" export type AuthenticatedQueryCtx = QueryCtx & { - user: Doc<"users"> + user: AuthUser identity: UserIdentity } export type AuthenticatedMutationCtx = MutationCtx & { - user: Doc<"users"> + user: AuthUser identity: UserIdentity } diff --git a/packages/convex/model/filesystem.ts b/packages/convex/model/filesystem.ts index 89bd47d..da3c8b6 100644 --- a/packages/convex/model/filesystem.ts +++ b/packages/convex/model/filesystem.ts @@ -2,6 +2,7 @@ import { v } from "convex/values" import type { Doc, Id } from "../_generated/dataModel" import type { AuthenticatedMutationCtx } from "../functions" import * as Directories from "./directories" +import * as Err from "./error" import * as Files from "./files" export enum FileType { @@ -81,6 +82,29 @@ export const VFileHandle = v.object({ }) export const VFileSystemHandle = v.union(VFileHandle, VDirectoryHandle) +export async function ensureRootDirectory( + ctx: AuthenticatedMutationCtx, +): Promise> { + const existing = await ctx.db + .query("directories") + .withIndex("byParentId", (q) => + q.eq("userId", ctx.user._id).eq("parentId", undefined), + ) + .first() + + if (existing) { + return existing._id + } + + const now = Date.now() + return await ctx.db.insert("directories", { + name: "", + createdAt: now, + updatedAt: now, + userId: ctx.user._id, + }) +} + /** * Recursively collects all file and directory handles from the given handles, * including all nested items. Only includes items that are in trash (deletedAt >= 0). diff --git a/packages/convex/model/user.ts b/packages/convex/model/user.ts index c62069d..af0bb36 100644 --- a/packages/convex/model/user.ts +++ b/packages/convex/model/user.ts @@ -1,54 +1,25 @@ import type { MutationCtx, QueryCtx } from "../_generated/server" -import type { AuthenticatedMutationCtx } from "../functions" +import { authComponent } from "../auth" import * as Err from "./error" +export type AuthUser = Awaited> + /** * Get the current authenticated user identity * Throws an error if the user is not authenticated */ export async function userIdentityOrThrow(ctx: QueryCtx | MutationCtx) { const identity = await ctx.auth.getUserIdentity() - if (!identity) { throw Err.create(Err.Code.Unauthenticated, "Not authenticated") } - return identity } /** - * Get internal user document from JWT authentication + * Get user document from JWT authentication * Throws an error if the user is not authenticated */ export async function userOrThrow(ctx: QueryCtx | MutationCtx) { - const identity = await userIdentityOrThrow(ctx) - - // Look for existing user by JWT subject - const user = await ctx.db - .query("users") - .withIndex("byJwtSubject", (q) => q.eq("jwtSubject", identity.subject)) - .first() - - if (!user) { - throw Err.create( - Err.Code.Unauthenticated, - "User not found - please sync user first", - ) - } - + const user = await authComponent.getAuthUser(ctx) return user } - -export async function register(ctx: AuthenticatedMutationCtx) { - const now = Date.now() - await Promise.all([ - ctx.db.insert("users", { - jwtSubject: ctx.identity.subject, - }), - ctx.db.insert("directories", { - name: "", - userId: ctx.user._id, - createdAt: now, - updatedAt: now, - }), - ]) -} diff --git a/packages/convex/schema.ts b/packages/convex/schema.ts index 6571c21..0979af9 100644 --- a/packages/convex/schema.ts +++ b/packages/convex/schema.ts @@ -2,12 +2,9 @@ import { defineSchema, defineTable } from "convex/server" import { v } from "convex/values" const schema = defineSchema({ - users: defineTable({ - jwtSubject: v.string(), - }).index("byJwtSubject", ["jwtSubject"]), files: defineTable({ storageId: v.id("_storage"), - userId: v.id("users"), + userId: v.string(), // BetterAuth user IDs are strings, not Convex Ids directoryId: v.optional(v.id("directories")), name: v.string(), size: v.number(), @@ -27,7 +24,7 @@ const schema = defineSchema({ ]), directories: defineTable({ name: v.string(), - userId: v.id("users"), + userId: v.string(), // BetterAuth user IDs are strings, not Convex Ids parentId: v.optional(v.id("directories")), createdAt: v.number(), updatedAt: v.number(), diff --git a/packages/convex/user.ts b/packages/convex/user.ts new file mode 100644 index 0000000..3388f64 --- /dev/null +++ b/packages/convex/user.ts @@ -0,0 +1,8 @@ +import { authenticatedMutation } from "./functions" +import * as FileSystem from "./model/filesystem" + +export const ensureRootDirectory = authenticatedMutation({ + handler: async (ctx) => { + return await FileSystem.ensureRootDirectory(ctx) + }, +}) diff --git a/packages/convex/users.ts b/packages/convex/users.ts deleted file mode 100644 index c8732d6..0000000 --- a/packages/convex/users.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { mutation } from "./_generated/server" -import { authenticatedQuery } from "./functions" -import * as Err from "./model/error" - -export const getCurrentUser = authenticatedQuery({ - handler: async (ctx) => { - // ctx.user is the internal Convex user document - return ctx.user - }, -}) - -export const syncUser = mutation({ - handler: async (ctx) => { - const identity = await ctx.auth.getUserIdentity() - if (!identity) { - throw Err.create(Err.Code.Unauthenticated) - } - - const existingUser = await ctx.db - .query("users") - .withIndex("byJwtSubject", (q) => - q.eq("jwtSubject", identity.subject), - ) - .first() - - if (!existingUser) { - await ctx.db.insert("users", { - jwtSubject: identity.subject, - }) - } - }, -}) diff --git a/packages/web/src/auth-client.ts b/packages/web/src/auth-client.ts deleted file mode 100644 index 4066402..0000000 --- a/packages/web/src/auth-client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - convexClient, - crossDomainClient, -} from "@convex-dev/better-auth/client/plugins" -import { createAuthClient } from "better-auth/react" - -export type AuthErrorCode = keyof typeof authClient.$ERROR_CODES - -export const authClient = createAuthClient({ - baseURL: process.env.BUN_PUBLIC_CONVEX_SITE_URL, - plugins: [convexClient(), crossDomainClient()], -}) diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts new file mode 100644 index 0000000..c9a1ba1 --- /dev/null +++ b/packages/web/src/auth.ts @@ -0,0 +1,27 @@ +import { + convexClient, + crossDomainClient, +} from "@convex-dev/better-auth/client/plugins" +import { createAuthClient } from "better-auth/react" +import { createContext, useContext } from "react" + +export type AuthErrorCode = keyof typeof authClient.$ERROR_CODES + +export const authClient = createAuthClient({ + baseURL: process.env.BUN_PUBLIC_CONVEX_SITE_URL, + plugins: [convexClient(), crossDomainClient()], +}) + +export type Session = NonNullable< + Awaited>["data"] +> + +export const SessionContext = createContext(null) + +export function useSession() { + const context = useContext(SessionContext) + if (!context) { + throw new Error("useSession must be used within a SessionProvider") + } + return context +} diff --git a/packages/web/src/routes/__root.tsx b/packages/web/src/routes/__root.tsx index 68dc6d3..3889540 100644 --- a/packages/web/src/routes/__root.tsx +++ b/packages/web/src/routes/__root.tsx @@ -7,7 +7,7 @@ import { toast } from "sonner" import { Toaster } from "@/components/ui/sonner" import { formatError } from "@/lib/error" import { useKeyboardModifierListener } from "@/lib/keyboard" -import { authClient } from "../auth-client" +import { authClient } from "../auth" export const Route = createRootRoute({ component: RootLayout, diff --git a/packages/web/src/routes/_authenticated.tsx b/packages/web/src/routes/_authenticated.tsx index faea5b7..724c94b 100644 --- a/packages/web/src/routes/_authenticated.tsx +++ b/packages/web/src/routes/_authenticated.tsx @@ -11,7 +11,7 @@ import { useConvexAuth, } from "convex/react" import { useEffect, useState } from "react" -import { authClient } from "@/auth-client" +import { authClient, SessionContext } from "@/auth" import { LoadingSpinner } from "@/components/ui/loading-spinner" export const Route = createFileRoute("/_authenticated")({ @@ -21,7 +21,7 @@ export const Route = createFileRoute("/_authenticated")({ function AuthenticatedLayout() { const { search } = useLocation() const { isLoading } = useConvexAuth() - const { isPending: sessionLoading } = authClient.useSession() + const { data: session, isPending: sessionLoading } = authClient.useSession() const [hasProcessedAuth, setHasProcessedAuth] = useState(false) // Check if we're in the middle of processing an auth code @@ -50,6 +50,11 @@ function AuthenticatedLayout() { return ( <> + {session ? ( + + + + ) : null} diff --git a/packages/web/src/routes/_authenticated/_sidebar-layout.tsx b/packages/web/src/routes/_authenticated/_sidebar-layout.tsx index 73159bb..654fbf3 100644 --- a/packages/web/src/routes/_authenticated/_sidebar-layout.tsx +++ b/packages/web/src/routes/_authenticated/_sidebar-layout.tsx @@ -16,7 +16,6 @@ function RouteComponent() { - ) } diff --git a/packages/web/src/routes/sign-up.tsx b/packages/web/src/routes/sign-up.tsx index c5cb9a2..8757382 100644 --- a/packages/web/src/routes/sign-up.tsx +++ b/packages/web/src/routes/sign-up.tsx @@ -19,7 +19,7 @@ import { FieldLabel, } from "@/components/ui/field" import { Input } from "@/components/ui/input" -import { type AuthErrorCode, authClient } from "../auth-client" +import { type AuthErrorCode, authClient } from "../auth" export const Route = createFileRoute("/sign-up")({ component: SignupPage, @@ -145,8 +145,6 @@ function SignupForm() { passwordFieldError = "Passwords do not match" } - console.log({ passwordFieldError }) - return (