Merge pull request #671 from CREDO23/sur-70-feature-streamline-onboarding-auto-create-default-workspace

[Feature] Streamline onboarding | Auto create default Search space
This commit is contained in:
Rohan Verma 2026-01-10 13:59:54 -08:00 committed by GitHub
commit 8492ea3ad1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 11 deletions

View file

@ -1,3 +1,4 @@
import logging
import uuid
from fastapi import Depends, Request, Response
@ -12,7 +13,17 @@ from fastapi_users.db import SQLAlchemyUserDatabase
from pydantic import BaseModel
from app.config import config
from app.db import User, get_user_db
from app.db import (
SearchSpace,
SearchSpaceMembership,
SearchSpaceRole,
User,
async_session_maker,
get_default_roles_config,
get_user_db,
)
logger = logging.getLogger(__name__)
class BearerResponse(BaseModel):
@ -36,7 +47,59 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
verification_token_secret = SECRET
async def on_after_register(self, user: User, request: Request | None = None):
print(f"User {user.id} has registered.")
"""
Called after a user registers. Creates a default search space for the user
so they can start chatting immediately without manual setup.
"""
logger.info(f"User {user.id} has registered. Creating default search space...")
try:
async with async_session_maker() as session:
# Create default search space
default_search_space = SearchSpace(
name="My Search Space",
description="Your personal search space",
user_id=user.id,
)
session.add(default_search_space)
await session.flush() # Get the search space ID
# Create default roles
default_roles = get_default_roles_config()
owner_role_id = None
for role_config in default_roles:
db_role = SearchSpaceRole(
name=role_config["name"],
description=role_config["description"],
permissions=role_config["permissions"],
is_default=role_config["is_default"],
is_system_role=role_config["is_system_role"],
search_space_id=default_search_space.id,
)
session.add(db_role)
await session.flush()
if role_config["name"] == "Owner":
owner_role_id = db_role.id
# Create owner membership
owner_membership = SearchSpaceMembership(
user_id=user.id,
search_space_id=default_search_space.id,
role_id=owner_role_id,
is_owner=True,
)
session.add(owner_membership)
await session.commit()
logger.info(
f"Created default search space (ID: {default_search_space.id}) for user {user.id}"
)
except Exception as e:
logger.error(
f"Failed to create default search space for user {user.id}: {e}"
)
async def on_after_forgot_password(
self, user: User, token: str, request: Request | None = None

View file

@ -7,6 +7,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
@ -129,6 +130,7 @@ const ErrorScreen = ({ message }: { message: string }) => {
const DashboardPage = () => {
const t = useTranslations("dashboard");
const tCommon = useTranslations("common");
const router = useRouter();
// Animation variants
const containerVariants: Variants = {
@ -164,6 +166,15 @@ const DashboardPage = () => {
const { data: user, isPending: isLoadingUser, error: userError } = useAtomValue(currentUserAtom);
// Auto-redirect to chat for users with exactly 1 search space
useEffect(() => {
if (loading) return;
if (searchSpaces.length === 1) {
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat`);
}
}, [loading, searchSpaces, router]);
// Create user object for UserDropdown
const customUser = {
name: user?.email ? user.email.split("@")[0] : "User",
@ -173,7 +184,8 @@ const DashboardPage = () => {
avatar: "/icon-128.svg", // Default avatar
};
if (loading) return <LoadingScreen />;
// Show loading while loading or auto-redirecting (single search space)
if (loading || (searchSpaces.length === 1 && !error)) return <LoadingScreen />;
if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />;
const handleDeleteSearchSpace = async (id: number) => {

View file

@ -7,7 +7,13 @@ import { cn } from "@/lib/utils";
export const Logo = ({ className }: { className?: string }) => {
return (
<Link href="/">
<Image src="/icon-128.svg" className={cn("dark:invert", className)} alt="logo" width={128} height={128} />
<Image
src="/icon-128.svg"
className={cn("dark:invert", className)}
alt="logo"
width={128}
height={128}
/>
</Link>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
import { trackLoginSuccess } from "@/lib/posthog/events";
@ -25,7 +25,6 @@ const TokenHandler = ({
tokenParamName = "token",
storageKey = "surfsense_bearer_token",
}: TokenHandlerProps) => {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
@ -58,14 +57,14 @@ const TokenHandler = ({
const finalRedirectPath = savedRedirectPath || redirectPath;
// Redirect to the appropriate path
router.push(finalRedirectPath);
window.location.href = finalRedirectPath;
} catch (error) {
console.error("Error storing token in localStorage:", error);
// Even if there's an error, try to redirect to the default path
router.push(redirectPath);
window.location.href = redirectPath;
}
}
}, [searchParams, tokenParamName, storageKey, redirectPath, router]);
}, [searchParams, tokenParamName, storageKey, redirectPath]);
return (
<div className="flex items-center justify-center min-h-[200px]">

View file

@ -34,14 +34,14 @@ export function UserDropdown({
if (typeof window !== "undefined") {
localStorage.removeItem("surfsense_bearer_token");
router.push("/");
window.location.href = "/";
}
} catch (error) {
console.error("Error during logout:", error);
// Optionally, provide user feedback
if (typeof window !== "undefined") {
alert("Logout failed. Please try again.");
router.push("/");
window.location.href = "/";
}
}
};