mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-22 15:45:22 +02:00
Merge remote-tracking branch 'refs/remotes/origin/canary' into feature/custom-entrypoint
# Conflicts: # apps/dokploy/drizzle/meta/0130_snapshot.json # apps/dokploy/drizzle/meta/_journal.json
This commit is contained in:
@@ -60,6 +60,30 @@ export const commonCronExpressions = [
|
||||
{ label: "Custom", value: "custom" },
|
||||
];
|
||||
|
||||
export const commonTimezones = [
|
||||
{ label: "UTC (Coordinated Universal Time)", value: "UTC" },
|
||||
{ label: "America/New_York (Eastern Time)", value: "America/New_York" },
|
||||
{ label: "America/Chicago (Central Time)", value: "America/Chicago" },
|
||||
{ label: "America/Denver (Mountain Time)", value: "America/Denver" },
|
||||
{ label: "America/Los_Angeles (Pacific Time)", value: "America/Los_Angeles" },
|
||||
{
|
||||
label: "America/Mexico_City (Central Mexico)",
|
||||
value: "America/Mexico_City",
|
||||
},
|
||||
{ label: "America/Sao_Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
|
||||
{ label: "Europe/London (Greenwich Mean Time)", value: "Europe/London" },
|
||||
{ label: "Europe/Paris (Central European Time)", value: "Europe/Paris" },
|
||||
{ label: "Europe/Berlin (Central European Time)", value: "Europe/Berlin" },
|
||||
{ label: "Asia/Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
|
||||
{ label: "Asia/Shanghai (China Standard Time)", value: "Asia/Shanghai" },
|
||||
{ label: "Asia/Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
|
||||
{ label: "Asia/Kolkata (India Standard Time)", value: "Asia/Kolkata" },
|
||||
{
|
||||
label: "Australia/Sydney (Australian Eastern Time)",
|
||||
value: "Australia/Sydney",
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
@@ -75,6 +99,7 @@ const formSchema = z
|
||||
"dokploy-server",
|
||||
]),
|
||||
script: z.string(),
|
||||
timezone: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.scheduleType === "compose" && !data.serviceName) {
|
||||
@@ -213,6 +238,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
serviceName: "",
|
||||
scheduleType: scheduleType || "application",
|
||||
script: "",
|
||||
timezone: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -251,6 +277,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
serviceName: schedule.serviceName || "",
|
||||
scheduleType: schedule.scheduleType,
|
||||
script: schedule.script || "",
|
||||
timezone: schedule.timezone || undefined,
|
||||
});
|
||||
}
|
||||
}, [form, schedule, scheduleId]);
|
||||
@@ -464,6 +491,54 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
formControl={form.control}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timezone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
Timezone
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Select a timezone for the schedule. If not
|
||||
specified, UTC will be used.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="UTC (default)" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{commonTimezones.map((tz) => (
|
||||
<SelectItem key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Optional: Choose a timezone for the schedule execution time
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{(scheduleTypeForm === "application" ||
|
||||
scheduleTypeForm === "compose") && (
|
||||
<>
|
||||
|
||||
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
1
apps/dokploy/drizzle/0130_perpetual_screwball.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "schedule" ADD COLUMN "timezone" text;
|
||||
1
apps/dokploy/drizzle/0131_sharp_ozymandias.sql
Normal file
1
apps/dokploy/drizzle/0131_sharp_ozymandias.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "domain" ADD COLUMN "customEntrypoint" text;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "0cf5b59d-2d0f-4f7e-88fd-726d739a1099",
|
||||
"id": "cff546ae-01ea-40d4-b32d-0512e05c3856",
|
||||
"prevId": "202e5dc1-5107-44cc-8c79-3f98a2fce763",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -2722,12 +2722,6 @@
|
||||
"notNull": false,
|
||||
"default": 3000
|
||||
},
|
||||
"customEntrypoint": {
|
||||
"name": "customEntrypoint",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
@@ -5771,6 +5765,12 @@
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"timezone": {
|
||||
"name": "timezone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "text",
|
||||
|
||||
6927
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
6927
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -915,8 +915,15 @@
|
||||
{
|
||||
"idx": 130,
|
||||
"version": "7",
|
||||
"when": 1765161998200,
|
||||
"tag": "0130_abandoned_dagger",
|
||||
"when": 1765167657813,
|
||||
"tag": "0130_perpetual_screwball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 131,
|
||||
"version": "7",
|
||||
"when": 1765274846213,
|
||||
"tag": "0131_sharp_ozymandias",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -32,8 +32,6 @@ export const organizationRouter = createTRPCRouter({
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
console.log("result", result);
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
@@ -82,7 +80,22 @@ export const organizationRouter = createTRPCRouter({
|
||||
organizationId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Verify user is a member of this organization
|
||||
const userMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, input.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!userMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
return await db.query.organization.findFirst({
|
||||
where: eq(organization.id, input.organizationId),
|
||||
});
|
||||
@@ -96,12 +109,45 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
// First, verify the organization exists
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, input.organizationId),
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Organization not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify user is a member of this organization
|
||||
const userMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, input.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!userMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can update the organization
|
||||
// Verify the user is either the organization owner or has the owner role
|
||||
const isOwner =
|
||||
org.ownerId === ctx.user.id || userMember.role === "owner";
|
||||
|
||||
if (!isOwner) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can update it",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.update(organization)
|
||||
.set({
|
||||
@@ -119,12 +165,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can delete it",
|
||||
});
|
||||
}
|
||||
// First, verify the organization exists
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, input.organizationId),
|
||||
});
|
||||
@@ -136,7 +177,27 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (org.ownerId !== ctx.user.id) {
|
||||
// Verify user is a member of this organization
|
||||
const userMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, input.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!userMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can delete the organization
|
||||
// Verify the user is either the organization owner or has the owner role
|
||||
const isOwner =
|
||||
org.ownerId === ctx.user.id || userMember.role === "owner";
|
||||
|
||||
if (!isOwner) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can delete it",
|
||||
|
||||
@@ -30,6 +30,7 @@ export const scheduleRouter = createTRPCRouter({
|
||||
scheduleId: newSchedule.scheduleId,
|
||||
type: "schedule",
|
||||
cronSchedule: newSchedule.cronExpression,
|
||||
timezone: newSchedule.timezone,
|
||||
});
|
||||
} else {
|
||||
scheduleJob(newSchedule);
|
||||
@@ -49,6 +50,7 @@ export const scheduleRouter = createTRPCRouter({
|
||||
scheduleId: updatedSchedule.scheduleId,
|
||||
type: "schedule",
|
||||
cronSchedule: updatedSchedule.cronExpression,
|
||||
timezone: updatedSchedule.timezone,
|
||||
});
|
||||
} else {
|
||||
await removeJob({
|
||||
|
||||
@@ -430,6 +430,23 @@ export const userRouter = createTRPCRouter({
|
||||
createApiKey: protectedProcedure
|
||||
.input(apiCreateApiKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Verify user is a member of the organization specified in metadata
|
||||
if (input.metadata?.organizationId) {
|
||||
const userMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, input.metadata.organizationId),
|
||||
eq(member.userId, ctx.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!userMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const apiKey = await createApiKey(ctx.user.id, input);
|
||||
return apiKey;
|
||||
}),
|
||||
@@ -440,7 +457,35 @@ export const userRouter = createTRPCRouter({
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
// Users can check their own organizations
|
||||
// Admins and owners can check organizations of members in their active organization
|
||||
if (input.userId !== ctx.user.id) {
|
||||
// Verify the target user is a member of the active organization
|
||||
const targetMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, input.userId),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
});
|
||||
|
||||
if (!targetMember) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User is not a member of your active organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Only admins and owners can check other users' organizations
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only admins and owners can check other users' organizations",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const organizations = await db.query.member.findMany({
|
||||
where: eq(member.userId, input.userId),
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ type QueueJob =
|
||||
type: "schedule";
|
||||
cronSchedule: string;
|
||||
scheduleId: string;
|
||||
timezone?: string | null;
|
||||
}
|
||||
| {
|
||||
type: "volume-backup";
|
||||
|
||||
@@ -40,6 +40,7 @@ export const scheduleJob = (job: QueueJob) => {
|
||||
jobQueue.add(job.scheduleId, job, {
|
||||
repeat: {
|
||||
pattern: job.cronSchedule,
|
||||
tz: job.timezone || "UTC",
|
||||
},
|
||||
});
|
||||
} else if (job.type === "volume-backup") {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const jobQueueSchema = z.discriminatedUnion("type", [
|
||||
cronSchedule: z.string(),
|
||||
type: z.literal("schedule"),
|
||||
scheduleId: z.string(),
|
||||
timezone: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
cronSchedule: z.string(),
|
||||
|
||||
@@ -49,6 +49,7 @@ export const schedules = pgTable("schedule", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
timezone: text("timezone"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
@@ -14,11 +14,21 @@ import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const scheduleJob = (schedule: Schedule) => {
|
||||
const { cronExpression, scheduleId } = schedule;
|
||||
const { cronExpression, scheduleId, timezone } = schedule;
|
||||
|
||||
scheduleJobNode(scheduleId, cronExpression, async () => {
|
||||
await runCommand(scheduleId);
|
||||
});
|
||||
// Use timezone from schedule, default to UTC if not specified
|
||||
const tz = timezone || "UTC";
|
||||
|
||||
scheduleJobNode(
|
||||
scheduleId,
|
||||
{
|
||||
tz,
|
||||
rule: cronExpression,
|
||||
},
|
||||
async () => {
|
||||
await runCommand(scheduleId);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const removeScheduleJob = (scheduleId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user