Skip to content

enforce connection management perms to owner #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 24 additions & 4 deletions packages/web/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export const createConnection = async (name: string, type: string, connectionCon
return {
id: connection.id,
}
})
}, OrgRole.OWNER)
));

export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
Expand Down Expand Up @@ -520,7 +520,7 @@ export const updateConnectionDisplayName = async (connectionId: number, name: st
return {
success: true,
}
})
}, OrgRole.OWNER)
));

export const updateConnectionConfigAndScheduleSync = async (connectionId: number, config: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
Expand Down Expand Up @@ -560,7 +560,7 @@ export const updateConnectionConfigAndScheduleSync = async (connectionId: number
return {
success: true,
}
})
}, OrgRole.OWNER)
));

export const flagConnectionForSync = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
Expand Down Expand Up @@ -623,7 +623,7 @@ export const deleteConnection = async (connectionId: number, domain: string): Pr
return {
success: true,
}
})
}, OrgRole.OWNER)
));

export const getCurrentUserRole = async (domain: string): Promise<OrgRole | ServiceError> => sew(() =>
Expand Down Expand Up @@ -1377,6 +1377,26 @@ export const getSubscriptionData = async (domain: string) => sew(() =>
})
));

export const getOrgMembership = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId,
userId: session.user.id,
}
}
});

if (!membership) {
return notFound();
}

return membership;
})
));

export const getOrgMembers = async (domain: string) => sew(() =>
withAuth(async (session) =>
withOrgMembership(session, domain, async ({ orgId }) => {
Expand Down
28 changes: 20 additions & 8 deletions packages/web/src/app/[domain]/components/repositorySnapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { RepoIndexingStatus } from "@sourcebot/db";
import { SymbolIcon } from "@radix-ui/react-icons";

export function RepositorySnapshot() {
export function RepositorySnapshot({ authEnabled }: { authEnabled: boolean }) {
const domain = useDomain();

const { data: repos, isPending, isError } = useQuery({
Expand Down Expand Up @@ -44,7 +44,7 @@ export function RepositorySnapshot() {
)
} else if (numIndexedRepos == 0) {
return (
<EmptyRepoState domain={domain} />
<EmptyRepoState domain={domain} authEnabled={authEnabled} />
)
}

Expand All @@ -65,19 +65,31 @@ export function RepositorySnapshot() {
)
}

function EmptyRepoState({ domain }: { domain: string }) {
function EmptyRepoState({ domain, authEnabled }: { domain: string, authEnabled: boolean }) {
return (
<div className="flex flex-col items-center gap-3">
<span className="text-sm">No repositories found</span>

<div className="w-full max-w-lg">
<div className="flex flex-row items-center gap-2 border rounded-md p-4 justify-center">
<span className="text-sm text-muted-foreground">
Create a{" "}
<Link href={`/${domain}/connections`} className="text-blue-500 hover:underline inline-flex items-center gap-1">
connection
</Link>{" "}
to start indexing repositories
{authEnabled ? (
<>
Create a{" "}
<Link href={`/${domain}/connections`} className="text-blue-500 hover:underline inline-flex items-center gap-1">
connection
</Link>{" "}
to start indexing repositories
</>
) : (
<>
Create a {" "}
<Link href={`https://docs.sourcebot.dev/self-hosting`} className="text-blue-500 hover:underline inline-flex items-center gap-1" target="_blank">
configuration file
</Link>{" "}
to start indexing repositories
</>
)}
</span>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ConfigSettingProps {
connectionId: number;
config: string;
type: string;
disabled?: boolean;
}

export const ConfigSetting = (props: ConfigSettingProps) => {
Expand Down Expand Up @@ -83,10 +84,12 @@ function ConfigSettingInternal<T>({
quickActions,
schema,
type,
disabled,
}: ConfigSettingProps & {
quickActions?: QuickAction<T>[],
schema: Schema,
type: CodeHostType,
disabled?: boolean,
}) {
const { toast } = useToast();
const router = useRouter();
Expand Down Expand Up @@ -237,7 +240,7 @@ function ConfigSettingInternal<T>({
<Button
size="sm"
type="submit"
disabled={isLoading}
disabled={isLoading || disabled}
>
{isLoading && <Loader2 className="animate-spin mr-2" />}
{isLoading ? 'Syncing...' : 'Save'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import useCaptureEvent from "@/hooks/useCaptureEvent";

interface DeleteConnectionSettingProps {
connectionId: number;
disabled?: boolean;
}

export const DeleteConnectionSetting = ({
connectionId,
disabled,
}: DeleteConnectionSettingProps) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -73,7 +75,7 @@ export const DeleteConnectionSetting = ({
<Button
variant="destructive"
className="mt-4"
disabled={isLoading}
disabled={isLoading || disabled}
>
{isLoading && <Loader2 className="animate-spin mr-2" />}
Delete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ const formSchema = z.object({
interface DisplayNameSettingProps {
connectionId: number;
name: string;
disabled?: boolean;
}

export const DisplayNameSetting = ({
connectionId,
name,
disabled,
}: DisplayNameSettingProps) => {
const { toast } = useToast();
const router = useRouter();
Expand Down Expand Up @@ -85,7 +87,7 @@ export const DisplayNameSetting = ({
<Button
size="sm"
type="submit"
disabled={isLoading}
disabled={isLoading || disabled}
>
{isLoading && <Loader2 className="animate-spin mr-2" />}
Save
Expand Down
16 changes: 13 additions & 3 deletions packages/web/src/app/[domain]/connections/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { DisplayNameSetting } from "./components/displayNameSetting"
import { RepoList } from "./components/repoList"
import { getConnectionByDomain } from "@/data/connection"
import { Overview } from "./components/overview"

import { getOrgMembership } from "@/actions"
import { isServiceError } from "@/lib/utils"
import { notFound } from "next/navigation"
import { OrgRole } from "@sourcebot/db"
interface ConnectionManagementPageProps {
params: {
domain: string
Expand All @@ -34,6 +37,12 @@ export default async function ConnectionManagementPage({ params, searchParams }:
return <NotFound className="flex w-full h-full items-center justify-center" message="Connection not found" />
}

const membership = await getOrgMembership(params.domain);
if (isServiceError(membership)) {
return notFound();
}

const isOwner = membership.role === OrgRole.OWNER;
const currentTab = searchParams.tab || "overview";

return (
Expand Down Expand Up @@ -81,13 +90,14 @@ export default async function ConnectionManagementPage({ params, searchParams }:
value="settings"
className="flex flex-col gap-6"
>
<DisplayNameSetting connectionId={connection.id} name={connection.name} />
<DisplayNameSetting connectionId={connection.id} name={connection.name} disabled={!isOwner} />
<ConfigSetting
connectionId={connection.id}
type={connection.connectionType}
config={JSON.stringify(connection.config, null, 2)}
disabled={!isOwner}
/>
<DeleteConnectionSetting connectionId={connection.id} />
<DeleteConnectionSetting connectionId={connection.id} disabled={!isOwner} />
</TabsContent>
</Tabs>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ConnectionListItemProps {
editedAt: Date;
syncedAt?: Date;
failedRepos?: { repoId: number, repoName: string }[];
disabled: boolean;
}

export const ConnectionListItem = ({
Expand All @@ -43,6 +44,7 @@ export const ConnectionListItem = ({
editedAt,
syncedAt,
failedRepos,
disabled,
}: ConnectionListItemProps) => {
const statusDisplayName = useMemo(() => {
switch (status) {
Expand Down Expand Up @@ -111,7 +113,7 @@ export const ConnectionListItem = ({
)
}
</p>
<ConnectionListItemManageButton id={id} />
<ConnectionListItemManageButton id={id} disabled={disabled} />
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { useDomain } from "@/hooks/useDomain";

interface ConnectionListItemManageButtonProps {
id: string;
disabled: boolean;
}

export const ConnectionListItemManageButton = ({
id
id,
disabled,
}: ConnectionListItemManageButtonProps) => {
const captureEvent = useCaptureEvent()
const router = useRouter();
Expand All @@ -21,9 +23,12 @@ export const ConnectionListItemManageButton = ({
variant="outline"
size={"sm"}
className="ml-4"
disabled={disabled}
onClick={() => {
captureEvent('wa_connection_list_item_manage_pressed', {})
router.push(`/${domain}/connections/${id}`)
if (!disabled) {
captureEvent('wa_connection_list_item_manage_pressed', {})
router.push(`/${domain}/connections/${id}`)
}
}}
>
Manage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import { useMemo, useState } from "react";
import { MultiSelect } from "@/components/ui/multi-select";
import { OrgRole } from "@sourcebot/db";

interface ConnectionListProps {
className?: string;
role: OrgRole;
}

const convertSyncStatus = (status: ConnectionSyncStatus) => {
Expand All @@ -35,6 +38,7 @@ const convertSyncStatus = (status: ConnectionSyncStatus) => {

export const ConnectionList = ({
className,
role,
}: ConnectionListProps) => {
const domain = useDomain();
const [searchQuery, setSearchQuery] = useState("");
Expand Down Expand Up @@ -127,6 +131,7 @@ export const ConnectionList = ({
repoId: repo.id,
repoName: repo.name,
}))}
disabled={role !== OrgRole.OWNER}
/>
))
) : (
Expand Down
Loading