Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,13 @@ Configure the required and optional Convex environment variables for your applic

#### A. Authentication (Required)

OS Chat uses Convex Auth for authentication with Google OAuth.
OS Chat uses Better Auth for authentication with Google OAuth.

1. **Initialize Convex Auth:**
1. **Configure Better Auth:**

```bash
# Initialize Convex Auth setup
bunx @convex-dev/auth
# Better Auth is already configured in the codebase
# Set up your Google OAuth credentials in the Convex dashboard
```

2. **Set up Google OAuth:**
Expand Down Expand Up @@ -274,7 +274,7 @@ bunx convex env set POLAR_WEBHOOK_SECRET your-polar-webhook-secret

**Reference Documentation:**

- [Convex Auth Setup Guide](https://labs.convex.dev/auth/setup)
- [Convex Better Auth Documentation](https://convex-better-auth.netlify.app/)
- [Google OAuth Configuration](https://labs.convex.dev/auth/config/oauth/google)
- [Cloudflare R2 Component](https://www.convex.dev/components/cloudflare-r2)
- [Polar Component Documentation](https://www.convex.dev/components/polar)
Expand Down Expand Up @@ -336,7 +336,7 @@ For production deployment:

- Verify OAuth credentials in Convex dashboard
- Check `SITE_URL` matches your development/production URL
- Ensure Convex Auth is properly configured
- Ensure Better Auth is properly configured

**API Key Issues**:

Expand All @@ -353,7 +353,7 @@ For production deployment:
**Need Help?**

- Check the [Convex Documentation](https://docs.convex.dev)
- Review the [Convex Auth Setup Guide](https://labs.convex.dev/auth/setup)
- Review the [Convex Better Auth Documentation](https://convex-better-auth.netlify.app/)
- See [Google OAuth Configuration](https://labs.convex.dev/auth/config/oauth/google) for authentication
- Configure [Cloudflare R2](https://www.convex.dev/components/cloudflare-r2) for file storage
- Get an [Exa API key](https://exa.ai/) for web search functionality
Expand Down
5 changes: 5 additions & 0 deletions app/api/auth/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { nextJsHandler } from "@convex-dev/better-auth/nextjs";

// Export GET and POST handlers for Better Auth
// This handles all auth routes like /api/auth/signin, /api/auth/callback/google, etc.
export const { GET, POST } = nextJsHandler();
4 changes: 2 additions & 2 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import type { AnthropicProviderOptions } from "@ai-sdk/anthropic";
import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
// import { withTracing } from '@posthog/ai';
import {
consumeStream,
Expand All @@ -24,6 +23,7 @@ import { searchTool } from "@/app/api/tools/search";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import type { Message } from "@/convex/schema/message";
import { getToken } from "@/lib/auth-server";
import { MODELS_MAP } from "@/lib/config";
import { calculateConnectorStatus } from "@/lib/connector-utils";
import { createAgentTool } from "@/lib/create-agent-tool";
Expand Down Expand Up @@ -507,7 +507,7 @@ export async function POST(req: Request) {
return createErrorResponse(new Error("Invalid 'model' provided."));
}

const token = await convexAuthNextjsToken();
const token = await getToken();

// Get current user first (needed for multiple operations below)
const user = await fetchQuery(api.users.getCurrentUser, {}, { token });
Expand Down
4 changes: 2 additions & 2 deletions app/api/composio/connect/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchQuery } from "convex/nextjs";
import { NextResponse } from "next/server";
import { api } from "@/convex/_generated/api";
import { getToken } from "@/lib/auth-server";
import { initiateConnection } from "@/lib/composio-server";
import { SUPPORTED_CONNECTORS } from "@/lib/config/tools";
import { createErrorResponse } from "@/lib/error-utils";
import type { ConnectorType } from "@/lib/types";

export async function POST(request: Request) {
try {
const token = await convexAuthNextjsToken();
const token = await getToken();
if (!token) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
Expand Down
4 changes: 2 additions & 2 deletions app/api/composio/disconnect/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchMutation, fetchQuery } from "convex/nextjs";
import { NextResponse } from "next/server";
import { api } from "@/convex/_generated/api";
import { getToken } from "@/lib/auth-server";
import { disconnectAccount } from "@/lib/composio-server";
import { SUPPORTED_CONNECTORS } from "@/lib/config/tools";
import { createErrorResponse } from "@/lib/error-utils";
import type { ConnectorType } from "@/lib/types";

export async function POST(request: Request) {
try {
const token = await convexAuthNextjsToken();
const token = await getToken();
if (!token) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
Expand Down
4 changes: 2 additions & 2 deletions app/api/composio/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchQuery } from "convex/nextjs";
import { NextResponse } from "next/server";
import { api } from "@/convex/_generated/api";
import { getToken } from "@/lib/auth-server";
import { waitForConnection } from "@/lib/composio-server";
import { createErrorResponse } from "@/lib/error-utils";

export async function GET(request: Request) {
try {
const token = await convexAuthNextjsToken();
const token = await getToken();
if (!token) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
Expand Down
4 changes: 2 additions & 2 deletions app/api/create-chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchMutation, fetchQuery } from "convex/nextjs";
import { PostHog } from "posthog-node";
import { z } from "zod";
import { api } from "@/convex/_generated/api";
import { getToken } from "@/lib/auth-server";
import { createErrorResponse } from "@/lib/error-utils";

export async function POST(request: Request) {
Expand All @@ -25,7 +25,7 @@ export async function POST(request: Request) {

const { title, model, personaId } = parseResult.data;

const token = await convexAuthNextjsToken();
const token = await getToken();

const user = await fetchQuery(api.users.getCurrentUser, {}, { token });

Expand Down
4 changes: 2 additions & 2 deletions app/api/rate-limits/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { getToken } from "@/lib/auth-server";
import { createErrorResponse } from "@/lib/error-utils";

export async function GET() {
try {
const token = await convexAuthNextjsToken();
const token = await getToken();

// If no valid token, the user is not authenticated
if (!token) {
Expand Down
8 changes: 4 additions & 4 deletions app/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
"use client";

import { useAuthActions } from "@convex-dev/auth/react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
import { authClient } from "@/lib/auth-client";
import { APP_NAME } from "@/lib/config";
import { HeaderGoBack } from "../components/header-go-back";

export default function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const { signIn } = useAuthActions();

async function handleSignInWithGoogle() {
try {
setIsLoading(true);
setError(null);

await signIn("google");
await authClient.signIn.social({
provider: "google",
});
} catch (err: unknown) {
setIsLoading(false);
// console.error('Error signing in with Google:', err);
Expand Down
4 changes: 2 additions & 2 deletions app/c/[chatId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
import { fetchQuery } from "convex/nextjs";
import { redirect } from "next/navigation";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { getToken } from "@/lib/auth-server";

import Chat from "../../components/chat/chat";

Expand All @@ -12,7 +12,7 @@ export default async function ChatPage({
params: Promise<{ chatId: string }>;
}) {
// Validate the chat on the server to avoid client-side flashes.
const token = await convexAuthNextjsToken();
const token = await getToken();

// If we fail to obtain a token (anonymous visitor with cookies disabled, etc.)
// we still attempt the query – Convex will treat it as anonymous and only
Expand Down
28 changes: 23 additions & 5 deletions app/components/auth/anonymous-sign-in.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
"use client";

import { useAuthActions } from "@convex-dev/auth/react";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Loader } from "@/components/prompt-kit/loader";
import { authClient } from "@/lib/auth-client";

export function AnonymousSignIn() {
const { signIn } = useAuthActions();
const attemptedAnon = useRef(false);
const [error, setError] = useState<string | null>(null);

// Handle anonymous sign-in when user is unauthenticated
useEffect(() => {
if (!attemptedAnon.current) {
attemptedAnon.current = true;
signIn("anonymous");
authClient.signIn.anonymous().catch(() => {
setError("Failed to sign in. Please refresh the page.");
});
}
}, [signIn]);
}, []);

// Show error if sign-in fails
if (error) {
return (
<div className="flex h-dvh flex-col items-center justify-center gap-4 bg-background">
<p className="text-destructive">{error}</p>
<button
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
onClick={() => window.location.reload()}
type="button"
>
Refresh Page
</button>
</div>
);
}

// Show loading while anonymous sign-in is processing
return (
Expand Down
6 changes: 2 additions & 4 deletions app/components/chat-input/popover-content-auth.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
"use client";

import { useAuthActions } from "@convex-dev/auth/react";
import Image from "next/image";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { PopoverContent } from "@/components/ui/popover";
import { authClient } from "@/lib/auth-client";
import { APP_NAME } from "../../../lib/config";

export function PopoverContentAuth() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const { signIn } = useAuthActions();

const handleSignInWithGoogle = async () => {
try {
setIsLoading(true);
setError(null);

await signIn("google");
await authClient.signIn.social({ provider: "google" });
} catch (_err: unknown) {
// console.error('Error signing in with Google:', err);
setError("Unable to sign in at the moment. Please try again later.");
Expand Down
8 changes: 4 additions & 4 deletions app/components/chat/dialog-auth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { useAuthActions } from "@convex-dev/auth/react";
import Image from "next/image";
import { useState } from "react";
import { Button } from "@/components/ui/button";
Expand All @@ -12,6 +11,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { authClient } from "@/lib/auth-client";

type DialogAuthProps = {
open: boolean;
Expand All @@ -22,13 +22,13 @@ export function DialogAuth({ open, setOpen }: DialogAuthProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const { signIn } = useAuthActions();

const handleSignInWithGoogle = async () => {
try {
setIsLoading(true);
setError(null);
await signIn("google");
await authClient.signIn.social({
provider: "google",
});
setOpen(false);
} catch (err: unknown) {
if (err instanceof Error) {
Expand Down
4 changes: 2 additions & 2 deletions app/hooks/use-model-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function useModelPreferences() {
}

const newFavorites = isFavorite
? favorites.filter((id) => id !== modelId)
? favorites.filter((id: string) => id !== modelId)
: [...favorites, modelId];

localStore.setQuery(
Expand All @@ -50,7 +50,7 @@ export function useModelPreferences() {

// Remove favorite models from disabled list (auto-enable favorites)
const newDisabled = currentDisabled.filter(
(id) => !newFavorites.includes(id)
(id: string) => !newFavorites.includes(id)
);

localStore.setQuery(
Expand Down
14 changes: 8 additions & 6 deletions app/hooks/use-model-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function useModelSettings() {

if (enabled) {
// Enabling model - remove from disabled list
newDisabled = currentDisabled.filter((id) => id !== modelId);
newDisabled = currentDisabled.filter((id: string) => id !== modelId);
} else {
// Disabling model - add to disabled list and remove from favorites
if (modelId === MODEL_DEFAULT) {
Expand All @@ -37,13 +37,13 @@ export function useModelSettings() {
newDisabled = currentDisabled.includes(modelId)
? currentDisabled
: [...currentDisabled, modelId];
newFavorites = currentFavorites.filter((id) => id !== modelId);
newFavorites = currentFavorites.filter((id: string) => id !== modelId);

// Ensure at least one favorite remains
if (newFavorites.length === 0 && currentFavorites.length > 0) {
const firstFavorite = currentFavorites[0];
newFavorites = [firstFavorite];
newDisabled = newDisabled.filter((id) => id !== firstFavorite);
newDisabled = newDisabled.filter((id: string) => id !== firstFavorite);
}
}

Expand All @@ -70,11 +70,13 @@ export function useModelSettings() {
const currentDisabled = currentUser.disabledModels ?? [];

// Filter out MODEL_DEFAULT from the models to disable
const modelsToDisable = modelIds.filter((id) => id !== MODEL_DEFAULT);
const modelsToDisable = modelIds.filter(
(id: string) => id !== MODEL_DEFAULT
);

// Remove disabled models from favorites
let newFavorites = currentFavorites.filter(
(id) => !modelsToDisable.includes(id)
(id: string) => !modelsToDisable.includes(id)
);
// Merge with existing disabled models
let newDisabled = [...new Set([...currentDisabled, ...modelsToDisable])];
Expand All @@ -83,7 +85,7 @@ export function useModelSettings() {
if (newFavorites.length === 0 && currentFavorites.length > 0) {
const firstFavorite = currentFavorites[0];
newFavorites = [firstFavorite];
newDisabled = newDisabled.filter((id) => id !== firstFavorite);
newDisabled = newDisabled.filter((id: string) => id !== firstFavorite);
}

localStore.setQuery(
Expand Down
Loading