mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-11 14:35:17 +00:00
feat(git-provider): enhance sharing and permissions management
- Added functionality to toggle sharing of Git providers with the organization. - Introduced a new column "sharedWithOrganization" in the git_provider table to track sharing status. - Updated user permissions to include accessedGitProviders, allowing for more granular access control. - Enhanced API routes to support fetching accessible Git providers based on user roles and permissions. - Implemented UI components for managing Git provider sharing and permissions in the dashboard.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
ImportIcon,
|
ImportIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -24,6 +25,13 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||||
@@ -39,6 +47,8 @@ export const ShowGitProviders = () => {
|
|||||||
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.gitProvider.remove.useMutation();
|
api.gitProvider.remove.useMutation();
|
||||||
|
const { mutateAsync: toggleShare } =
|
||||||
|
api.gitProvider.toggleShare.useMutation();
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
const getGitlabUrl = (
|
const getGitlabUrl = (
|
||||||
@@ -154,10 +164,62 @@ export const ShowGitProviders = () => {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{!gitProvider.isOwner && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Users className="size-3 mr-1" />
|
||||||
|
Shared
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1 items-center">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
|
{gitProvider.isOwner && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center gap-1.5 mr-2">
|
||||||
|
<Users className="size-4 text-muted-foreground" />
|
||||||
|
<Switch
|
||||||
|
checked={
|
||||||
|
gitProvider.sharedWithOrganization
|
||||||
|
}
|
||||||
|
onCheckedChange={async (
|
||||||
|
checked,
|
||||||
|
) => {
|
||||||
|
await toggleShare({
|
||||||
|
gitProviderId:
|
||||||
|
gitProvider.gitProviderId,
|
||||||
|
sharedWithOrganization:
|
||||||
|
checked,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
checked
|
||||||
|
? "Provider shared with organization"
|
||||||
|
: "Provider unshared",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error updating sharing",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Share with entire organization
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBitbucket &&
|
{isBitbucket &&
|
||||||
gitProvider.bitbucket?.appPassword &&
|
gitProvider.bitbucket?.appPassword &&
|
||||||
!gitProvider.bitbucket?.apiToken ? (
|
!gitProvider.bitbucket?.apiToken ? (
|
||||||
@@ -222,62 +284,75 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGithub && haveGithubRequirements && (
|
{gitProvider.isOwner && (
|
||||||
<EditGithubProvider
|
<>
|
||||||
githubId={gitProvider.github?.githubId}
|
{isGithub && haveGithubRequirements && (
|
||||||
/>
|
<EditGithubProvider
|
||||||
)}
|
githubId={
|
||||||
|
gitProvider.github?.githubId
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isGitlab && (
|
{isGitlab && (
|
||||||
<EditGitlabProvider
|
<EditGitlabProvider
|
||||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
gitlabId={
|
||||||
/>
|
gitProvider.gitlab?.gitlabId
|
||||||
)}
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBitbucket && (
|
{isBitbucket && (
|
||||||
<EditBitbucketProvider
|
<EditBitbucketProvider
|
||||||
bitbucketId={
|
bitbucketId={
|
||||||
gitProvider.bitbucket?.bitbucketId
|
gitProvider.bitbucket?.bitbucketId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGitea && (
|
{isGitea && (
|
||||||
<EditGiteaProvider
|
<EditGiteaProvider
|
||||||
giteaId={gitProvider.gitea?.giteaId}
|
giteaId={gitProvider.gitea?.giteaId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Git Provider"
|
title="Delete Git Provider"
|
||||||
description="Are you sure you want to delete this Git Provider?"
|
description={
|
||||||
type="destructive"
|
gitProvider.sharedWithOrganization
|
||||||
onClick={async () => {
|
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
|
||||||
await mutateAsync({
|
: "Are you sure you want to delete this Git Provider?"
|
||||||
gitProviderId: gitProvider.gitProviderId,
|
}
|
||||||
})
|
type="destructive"
|
||||||
.then(() => {
|
onClick={async () => {
|
||||||
toast.success(
|
await mutateAsync({
|
||||||
"Git Provider deleted successfully",
|
gitProviderId:
|
||||||
);
|
gitProvider.gitProviderId,
|
||||||
refetch();
|
})
|
||||||
})
|
.then(() => {
|
||||||
.catch(() => {
|
toast.success(
|
||||||
toast.error(
|
"Git Provider deleted successfully",
|
||||||
"Error deleting Git Provider",
|
);
|
||||||
);
|
refetch();
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error(
|
||||||
<Button
|
"Error deleting Git Provider",
|
||||||
variant="ghost"
|
);
|
||||||
size="icon"
|
});
|
||||||
className="group hover:bg-red-500/10"
|
}}
|
||||||
isLoading={isRemoving}
|
>
|
||||||
>
|
<Button
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
variant="ghost"
|
||||||
</Button>
|
size="icon"
|
||||||
</DialogAction>
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
import { api, type RouterOutputs } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
|
|
||||||
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
||||||
@@ -170,6 +171,7 @@ const addPermissions = z.object({
|
|||||||
accessedProjects: z.array(z.string()).optional(),
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
accessedEnvironments: z.array(z.string()).optional(),
|
accessedEnvironments: z.array(z.string()).optional(),
|
||||||
accessedServices: z.array(z.string()).optional(),
|
accessedServices: z.array(z.string()).optional(),
|
||||||
|
accessedGitProviders: z.array(z.string()).optional(),
|
||||||
canCreateProjects: z.boolean().optional().default(false),
|
canCreateProjects: z.boolean().optional().default(false),
|
||||||
canCreateServices: z.boolean().optional().default(false),
|
canCreateServices: z.boolean().optional().default(false),
|
||||||
canDeleteProjects: z.boolean().optional().default(false),
|
canDeleteProjects: z.boolean().optional().default(false),
|
||||||
@@ -196,6 +198,15 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
});
|
});
|
||||||
|
const { data: haveValidLicense } =
|
||||||
|
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||||
|
|
||||||
|
const { data: gitProviders } = api.gitProvider.allForPermissions.useQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
enabled: isOpen && !!haveValidLicense,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { data, refetch } = api.user.one.useQuery(
|
const { data, refetch } = api.user.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -214,6 +225,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
accessedProjects: [],
|
accessedProjects: [],
|
||||||
accessedEnvironments: [],
|
accessedEnvironments: [],
|
||||||
accessedServices: [],
|
accessedServices: [],
|
||||||
|
accessedGitProviders: [],
|
||||||
canDeleteEnvironments: false,
|
canDeleteEnvironments: false,
|
||||||
canCreateProjects: false,
|
canCreateProjects: false,
|
||||||
canCreateServices: false,
|
canCreateServices: false,
|
||||||
@@ -235,6 +247,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
|
accessedGitProviders: data.accessedGitProviders || [],
|
||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
@@ -262,6 +275,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
|
accessedGitProviders: data.accessedGitProviders || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
@@ -870,6 +884,81 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{haveValidLicense ? (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="accessedGitProviders"
|
||||||
|
render={() => (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<div className="mb-4">
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Git Providers
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Select the Git Providers that the user can access
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
{gitProviders?.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No git providers found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="grid md:grid-cols-1 gap-2">
|
||||||
|
{gitProviders?.map((provider) => (
|
||||||
|
<FormField
|
||||||
|
key={provider.gitProviderId}
|
||||||
|
control={form.control}
|
||||||
|
name="accessedGitProviders"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-3 space-y-0 rounded-lg border p-3">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value?.includes(
|
||||||
|
provider.gitProviderId,
|
||||||
|
)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
field.onChange([
|
||||||
|
...(field.value || []),
|
||||||
|
provider.gitProviderId,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
field.onChange(
|
||||||
|
field.value?.filter(
|
||||||
|
(v) =>
|
||||||
|
v !== provider.gitProviderId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel className="text-sm cursor-pointer">
|
||||||
|
{provider.name}
|
||||||
|
</FormLabel>
|
||||||
|
<span className="text-xs text-muted-foreground capitalize">
|
||||||
|
({provider.providerType})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<EnterpriseFeatureLocked
|
||||||
|
compact
|
||||||
|
title="Git Provider Assignment"
|
||||||
|
description="Assign specific Git Providers to users with an Enterprise license."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "accessedGitProviders" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "git_provider" ADD COLUMN "sharedWithOrganization" boolean DEFAULT false NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1100,6 +1100,13 @@
|
|||||||
"when": 1774910955774,
|
"when": 1774910955774,
|
||||||
"tag": "0156_fair_vargas",
|
"tag": "0156_fair_vargas",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 157,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775246622387,
|
||||||
|
"tag": "0157_stiff_misty_knight",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createBitbucket,
|
createBitbucket,
|
||||||
findBitbucketById,
|
findBitbucketById,
|
||||||
|
getAccessibleGitProviderIds,
|
||||||
getBitbucketBranches,
|
getBitbucketBranches,
|
||||||
getBitbucketRepositories,
|
getBitbucketRepositories,
|
||||||
testBitbucketConnection,
|
testBitbucketConnection,
|
||||||
@@ -54,6 +55,8 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
return await findBitbucketById(input.bitbucketId);
|
return await findBitbucketById(input.bitbucketId);
|
||||||
}),
|
}),
|
||||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||||
|
|
||||||
let result = await db.query.bitbucket.findMany({
|
let result = await db.query.bitbucket.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitProvider: true,
|
gitProvider: true,
|
||||||
@@ -67,7 +70,7 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
return (
|
return (
|
||||||
provider.gitProvider.organizationId ===
|
provider.gitProvider.organizationId ===
|
||||||
ctx.session.activeOrganizationId &&
|
ctx.session.activeOrganizationId &&
|
||||||
provider.gitProvider.userId === ctx.session.userId
|
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
|
import {
|
||||||
|
findGitProviderById,
|
||||||
|
getAccessibleGitProviderIds,
|
||||||
|
removeGitProvider,
|
||||||
|
updateGitProvider,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { desc, eq, inArray } from "drizzle-orm";
|
||||||
import { audit } from "@/server/api/utils/audit";
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
withPermission,
|
withPermission,
|
||||||
} from "@/server/api/trpc";
|
} from "@/server/api/trpc";
|
||||||
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
import {
|
||||||
|
apiRemoveGitProvider,
|
||||||
|
apiToggleShareGitProvider,
|
||||||
|
gitProvider,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const gitProviderRouter = createTRPCRouter({
|
export const gitProviderRouter = createTRPCRouter({
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.gitProvider.findMany({
|
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||||
|
|
||||||
|
if (accessibleIds.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await db.query.gitProvider.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitlab: true,
|
gitlab: true,
|
||||||
bitbucket: true,
|
bitbucket: true,
|
||||||
@@ -20,12 +36,67 @@ export const gitProviderRouter = createTRPCRouter({
|
|||||||
gitea: true,
|
gitea: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(gitProvider.createdAt),
|
orderBy: desc(gitProvider.createdAt),
|
||||||
where: and(
|
where: inArray(gitProvider.gitProviderId, [...accessibleIds]),
|
||||||
eq(gitProvider.userId, ctx.session.userId),
|
|
||||||
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return results.map((r) => ({
|
||||||
|
...r,
|
||||||
|
isOwner: r.userId === ctx.session.userId,
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
toggleShare: protectedProcedure
|
||||||
|
.input(apiToggleShareGitProvider)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const provider = await findGitProviderById(input.gitProviderId);
|
||||||
|
|
||||||
|
if (provider.userId !== ctx.session.userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Only the owner can share this provider",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceId: provider.gitProviderId,
|
||||||
|
resourceName: provider.name ?? provider.gitProviderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await updateGitProvider(input.gitProviderId, {
|
||||||
|
sharedWithOrganization: input.sharedWithOrganization,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
allForPermissions: withPermission("member", "update")
|
||||||
|
.use(async ({ ctx, next }) => {
|
||||||
|
const licensed = await hasValidLicense(
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (!licensed) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Valid enterprise license required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
})
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
return await db.query.gitProvider.findMany({
|
||||||
|
columns: {
|
||||||
|
gitProviderId: true,
|
||||||
|
name: true,
|
||||||
|
providerType: true,
|
||||||
|
},
|
||||||
|
orderBy: desc(gitProvider.createdAt),
|
||||||
|
where: eq(
|
||||||
|
gitProvider.organizationId,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
remove: withPermission("gitProviders", "delete")
|
remove: withPermission("gitProviders", "delete")
|
||||||
.input(apiRemoveGitProvider)
|
.input(apiRemoveGitProvider)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createGitea,
|
createGitea,
|
||||||
findGiteaById,
|
findGiteaById,
|
||||||
|
getAccessibleGitProviderIds,
|
||||||
getGiteaBranches,
|
getGiteaBranches,
|
||||||
getGiteaRepositories,
|
getGiteaRepositories,
|
||||||
haveGiteaRequirements,
|
haveGiteaRequirements,
|
||||||
@@ -57,6 +58,8 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||||
|
|
||||||
let result = await db.query.gitea.findMany({
|
let result = await db.query.gitea.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitProvider: true,
|
gitProvider: true,
|
||||||
@@ -67,7 +70,7 @@ export const giteaRouter = createTRPCRouter({
|
|||||||
(provider) =>
|
(provider) =>
|
||||||
provider.gitProvider.organizationId ===
|
provider.gitProvider.organizationId ===
|
||||||
ctx.session.activeOrganizationId &&
|
ctx.session.activeOrganizationId &&
|
||||||
provider.gitProvider.userId === ctx.session.userId,
|
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filtered = result
|
const filtered = result
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
findGithubById,
|
findGithubById,
|
||||||
|
getAccessibleGitProviderIds,
|
||||||
getGithubBranches,
|
getGithubBranches,
|
||||||
getGithubRepositories,
|
getGithubRepositories,
|
||||||
haveGithubRequirements,
|
haveGithubRequirements,
|
||||||
@@ -35,6 +36,8 @@ export const githubRouter = createTRPCRouter({
|
|||||||
return await getGithubBranches(input);
|
return await getGithubBranches(input);
|
||||||
}),
|
}),
|
||||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||||
|
|
||||||
let result = await db.query.github.findMany({
|
let result = await db.query.github.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitProvider: true,
|
gitProvider: true,
|
||||||
@@ -45,7 +48,7 @@ export const githubRouter = createTRPCRouter({
|
|||||||
(provider) =>
|
(provider) =>
|
||||||
provider.gitProvider.organizationId ===
|
provider.gitProvider.organizationId ===
|
||||||
ctx.session.activeOrganizationId &&
|
ctx.session.activeOrganizationId &&
|
||||||
provider.gitProvider.userId === ctx.session.userId,
|
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filtered = result
|
const filtered = result
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createGitlab,
|
createGitlab,
|
||||||
findGitlabById,
|
findGitlabById,
|
||||||
|
getAccessibleGitProviderIds,
|
||||||
getGitlabBranches,
|
getGitlabBranches,
|
||||||
getGitlabRepositories,
|
getGitlabRepositories,
|
||||||
haveGitlabRequirements,
|
haveGitlabRequirements,
|
||||||
@@ -54,6 +55,8 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
return await findGitlabById(input.gitlabId);
|
return await findGitlabById(input.gitlabId);
|
||||||
}),
|
}),
|
||||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||||
|
|
||||||
let result = await db.query.gitlab.findMany({
|
let result = await db.query.gitlab.findMany({
|
||||||
with: {
|
with: {
|
||||||
gitProvider: true,
|
gitProvider: true,
|
||||||
@@ -64,7 +67,7 @@ export const gitlabRouter = createTRPCRouter({
|
|||||||
return (
|
return (
|
||||||
provider.gitProvider.organizationId ===
|
provider.gitProvider.organizationId ===
|
||||||
ctx.session.activeOrganizationId &&
|
ctx.session.activeOrganizationId &&
|
||||||
provider.gitProvider.userId === ctx.session.userId
|
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const filtered = result
|
const filtered = result
|
||||||
|
|||||||
@@ -143,7 +143,12 @@ export const licenseKeyRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await deactivateLicenseKey(currentUser.licenseKey);
|
try {
|
||||||
|
await deactivateLicenseKey(currentUser.licenseKey);
|
||||||
|
} catch (_) {
|
||||||
|
// Always clean up locally even if the license server is unreachable
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
updateUser,
|
updateUser,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||||
import {
|
import {
|
||||||
account,
|
account,
|
||||||
apiAssignPermissions,
|
apiAssignPermissions,
|
||||||
@@ -344,12 +345,19 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, ...rest } = input;
|
const { id, accessedGitProviders, ...rest } = input;
|
||||||
|
|
||||||
|
const licensed = await hasValidLicense(
|
||||||
|
ctx.session?.activeOrganizationId || "",
|
||||||
|
);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
.set({
|
.set({
|
||||||
...rest,
|
...rest,
|
||||||
|
...(licensed && accessedGitProviders !== undefined
|
||||||
|
? { accessedGitProviders }
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ export const member = pgTable("member", {
|
|||||||
.array()
|
.array()
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`ARRAY[]::text[]`),
|
.default(sql`ARRAY[]::text[]`),
|
||||||
|
accessedGitProviders: text("accessedGitProviders")
|
||||||
|
.array()
|
||||||
|
.notNull()
|
||||||
|
.default(sql`ARRAY[]::text[]`),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const memberRelations = relations(member, ({ one }) => ({
|
export const memberRelations = relations(member, ({ one }) => ({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { organization } from "./account";
|
import { organization } from "./account";
|
||||||
@@ -32,6 +32,9 @@ export const gitProvider = pgTable("git_provider", {
|
|||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: "cascade" }),
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
sharedWithOrganization: boolean("sharedWithOrganization")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||||
@@ -64,3 +67,8 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
|||||||
export const apiRemoveGitProvider = z.object({
|
export const apiRemoveGitProvider = z.object({
|
||||||
gitProviderId: z.string().min(1),
|
gitProviderId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiToggleShareGitProvider = z.object({
|
||||||
|
gitProviderId: z.string().min(1),
|
||||||
|
sharedWithOrganization: z.boolean(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export const apiAssignPermissions = createSchema
|
|||||||
accessedProjects: z.array(z.string()).optional(),
|
accessedProjects: z.array(z.string()).optional(),
|
||||||
accessedEnvironments: z.array(z.string()).optional(),
|
accessedEnvironments: z.array(z.string()).optional(),
|
||||||
accessedServices: z.array(z.string()).optional(),
|
accessedServices: z.array(z.string()).optional(),
|
||||||
|
accessedGitProviders: z.array(z.string()).optional(),
|
||||||
canCreateProjects: z.boolean().optional(),
|
canCreateProjects: z.boolean().optional(),
|
||||||
canCreateServices: z.boolean().optional(),
|
canCreateServices: z.boolean().optional(),
|
||||||
canDeleteProjects: z.boolean().optional(),
|
canDeleteProjects: z.boolean().optional(),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { gitProvider } from "@dokploy/server/db/schema";
|
import { gitProvider, member } from "@dokploy/server/db/schema";
|
||||||
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
export type GitProvider = typeof gitProvider.$inferSelect;
|
export type GitProvider = typeof gitProvider.$inferSelect;
|
||||||
|
|
||||||
@@ -41,3 +42,51 @@ export const updateGitProvider = async (
|
|||||||
.returning()
|
.returning()
|
||||||
.then((response) => response[0]);
|
.then((response) => response[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAccessibleGitProviderIds = async (session: {
|
||||||
|
userId: string;
|
||||||
|
activeOrganizationId: string;
|
||||||
|
}): Promise<Set<string>> => {
|
||||||
|
const { userId, activeOrganizationId } = session;
|
||||||
|
|
||||||
|
const allOrgProviders = await db.query.gitProvider.findMany({
|
||||||
|
where: eq(gitProvider.organizationId, activeOrganizationId),
|
||||||
|
columns: {
|
||||||
|
gitProviderId: true,
|
||||||
|
userId: true,
|
||||||
|
sharedWithOrganization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberRecord = await db.query.member.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(member.userId, userId),
|
||||||
|
eq(member.organizationId, activeOrganizationId),
|
||||||
|
),
|
||||||
|
columns: { accessedGitProviders: true, role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
memberRecord?.role === "owner" ||
|
||||||
|
memberRecord?.role === "admin"
|
||||||
|
) {
|
||||||
|
return new Set(allOrgProviders.map((p) => p.gitProviderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const licensed = await hasValidLicense(activeOrganizationId);
|
||||||
|
const assignedSet = licensed
|
||||||
|
? new Set(memberRecord?.accessedGitProviders ?? [])
|
||||||
|
: new Set<string>();
|
||||||
|
|
||||||
|
const result = new Set<string>();
|
||||||
|
for (const p of allOrgProviders) {
|
||||||
|
if (
|
||||||
|
p.userId === userId ||
|
||||||
|
p.sharedWithOrganization ||
|
||||||
|
assignedSet.has(p.gitProviderId)
|
||||||
|
) {
|
||||||
|
result.add(p.gitProviderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user