mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +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,
|
||||
Loader2,
|
||||
Trash2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
@@ -24,6 +25,13 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} 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 { useUrl } from "@/utils/hooks/use-url";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
@@ -39,6 +47,8 @@ export const ShowGitProviders = () => {
|
||||
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
||||
const { mutateAsync, isPending: isRemoving } =
|
||||
api.gitProvider.remove.useMutation();
|
||||
const { mutateAsync: toggleShare } =
|
||||
api.gitProvider.toggleShare.useMutation();
|
||||
const url = useUrl();
|
||||
|
||||
const getGitlabUrl = (
|
||||
@@ -154,10 +164,62 @@ export const ShowGitProviders = () => {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{!gitProvider.isOwner && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
<Users className="size-3 mr-1" />
|
||||
Shared
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 &&
|
||||
gitProvider.bitbucket?.appPassword &&
|
||||
!gitProvider.bitbucket?.apiToken ? (
|
||||
@@ -222,62 +284,75 @@ export const ShowGitProviders = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github?.githubId}
|
||||
/>
|
||||
)}
|
||||
{gitProvider.isOwner && (
|
||||
<>
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={
|
||||
gitProvider.github?.githubId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||
/>
|
||||
)}
|
||||
{isGitlab && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={
|
||||
gitProvider.gitlab?.gitlabId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={
|
||||
gitProvider.bitbucket?.bitbucketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
{isGitea && (
|
||||
<EditGiteaProvider
|
||||
giteaId={gitProvider.gitea?.giteaId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description="Are you sure you want to delete this Git Provider?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId: gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Git Provider deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting Git Provider",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Delete Git Provider"
|
||||
description={
|
||||
gitProvider.sharedWithOrganization
|
||||
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
|
||||
: "Are you sure you want to delete this Git Provider?"
|
||||
}
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
gitProviderId:
|
||||
gitProvider.gitProviderId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Git Provider deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting Git Provider",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
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>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
/** 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(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
accessedGitProviders: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: 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, {
|
||||
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(
|
||||
{
|
||||
@@ -214,6 +225,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: [],
|
||||
accessedEnvironments: [],
|
||||
accessedServices: [],
|
||||
accessedGitProviders: [],
|
||||
canDeleteEnvironments: false,
|
||||
canCreateProjects: false,
|
||||
canCreateServices: false,
|
||||
@@ -235,6 +247,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
accessedGitProviders: data.accessedGitProviders || [],
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
@@ -262,6 +275,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
accessedGitProviders: data.accessedGitProviders || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
@@ -870,6 +884,81 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||
</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">
|
||||
<Button
|
||||
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,
|
||||
"tag": "0156_fair_vargas",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 157,
|
||||
"version": "7",
|
||||
"when": 1775246622387,
|
||||
"tag": "0157_stiff_misty_knight",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createBitbucket,
|
||||
findBitbucketById,
|
||||
getAccessibleGitProviderIds,
|
||||
getBitbucketBranches,
|
||||
getBitbucketRepositories,
|
||||
testBitbucketConnection,
|
||||
@@ -54,6 +55,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
return await findBitbucketById(input.bitbucketId);
|
||||
}),
|
||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.bitbucket.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -67,7 +70,7 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||
);
|
||||
});
|
||||
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 { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
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 {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
||||
import {
|
||||
apiRemoveGitProvider,
|
||||
apiToggleShareGitProvider,
|
||||
gitProvider,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
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: {
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
@@ -20,12 +36,67 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
gitea: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: and(
|
||||
eq(gitProvider.userId, ctx.session.userId),
|
||||
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
where: inArray(gitProvider.gitProviderId, [...accessibleIds]),
|
||||
});
|
||||
|
||||
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")
|
||||
.input(apiRemoveGitProvider)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createGitea,
|
||||
findGiteaById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGiteaBranches,
|
||||
getGiteaRepositories,
|
||||
haveGiteaRequirements,
|
||||
@@ -57,6 +58,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitea.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -67,7 +70,7 @@ export const giteaRouter = createTRPCRouter({
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
findGithubById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGithubBranches,
|
||||
getGithubRepositories,
|
||||
haveGithubRequirements,
|
||||
@@ -35,6 +36,8 @@ export const githubRouter = createTRPCRouter({
|
||||
return await getGithubBranches(input);
|
||||
}),
|
||||
githubProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.github.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -45,7 +48,7 @@ export const githubRouter = createTRPCRouter({
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId),
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
createGitlab,
|
||||
findGitlabById,
|
||||
getAccessibleGitProviderIds,
|
||||
getGitlabBranches,
|
||||
getGitlabRepositories,
|
||||
haveGitlabRequirements,
|
||||
@@ -54,6 +55,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return await findGitlabById(input.gitlabId);
|
||||
}),
|
||||
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
|
||||
|
||||
let result = await db.query.gitlab.findMany({
|
||||
with: {
|
||||
gitProvider: true,
|
||||
@@ -64,7 +67,7 @@ export const gitlabRouter = createTRPCRouter({
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
accessibleIds.has(provider.gitProvider.gitProviderId)
|
||||
);
|
||||
});
|
||||
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
|
||||
.update(user)
|
||||
.set({
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import {
|
||||
account,
|
||||
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
|
||||
.update(member)
|
||||
.set({
|
||||
...rest,
|
||||
...(licensed && accessedGitProviders !== undefined
|
||||
? { accessedGitProviders }
|
||||
: {}),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
|
||||
@@ -163,6 +163,10 @@ export const member = pgTable("member", {
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accessedGitProviders: text("accessedGitProviders")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
});
|
||||
|
||||
export const memberRelations = relations(member, ({ one }) => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
@@ -32,6 +32,9 @@ export const gitProvider = pgTable("git_provider", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
sharedWithOrganization: boolean("sharedWithOrganization")
|
||||
.notNull()
|
||||
.default(false),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -64,3 +67,8 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
export const apiRemoveGitProvider = z.object({
|
||||
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(),
|
||||
accessedEnvironments: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
accessedGitProviders: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
canDeleteProjects: z.boolean().optional(),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export type GitProvider = typeof gitProvider.$inferSelect;
|
||||
|
||||
@@ -41,3 +42,51 @@ export const updateGitProvider = async (
|
||||
.returning()
|
||||
.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