mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into ayham291/canary
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
28
SECURITY.md
Normal file
28
SECURITY.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Dokploy Security Policy
|
||||
|
||||
At Dokploy, security is a top priority. We appreciate the help of security researchers and the community in identifying and reporting vulnerabilities.
|
||||
|
||||
## How to Report a Vulnerability
|
||||
|
||||
If you have discovered a security vulnerability in Dokploy, we ask that you report it responsibly by following these guidelines:
|
||||
|
||||
1. **Contact us:** Send an email to [contact@dokploy.com](mailto:contact@dokploy.com).
|
||||
2. **Provide clear details:** Include as much information as possible to help us understand and reproduce the vulnerability. This should include:
|
||||
* A clear description of the vulnerability.
|
||||
* Steps to reproduce the vulnerability.
|
||||
* Any sample code, screenshots, or videos that might be helpful.
|
||||
* The potential impact of the vulnerability.
|
||||
3. **Do not make the vulnerability public:** Please refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address it. This is crucial for protecting our users.
|
||||
4. **Allow us time:** We will endeavor to acknowledge receipt of your report as soon as possible and keep you informed of our progress. The time to resolve the vulnerability may vary depending on its complexity and severity.
|
||||
|
||||
## What We Expect From You
|
||||
|
||||
* Do not access user data or systems beyond what is necessary to demonstrate the vulnerability.
|
||||
* Do not perform denial-of-service (DoS) attacks, spamming, or social engineering.
|
||||
* Do not modify or destroy data that does not belong to you.
|
||||
|
||||
## Our Commitment
|
||||
|
||||
We are committed to working with you quickly and responsibly to address any legitimate security vulnerability.
|
||||
|
||||
Thank you for helping us keep Dokploy secure for everyone.
|
||||
@@ -247,7 +247,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
@@ -256,7 +256,7 @@ export const UpdateVolume = ({
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
className="h-96 font-mono w-full"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ValidationState = {
|
||||
error?: string;
|
||||
resolvedIp?: string;
|
||||
message?: string;
|
||||
cdnProvider?: string;
|
||||
};
|
||||
|
||||
export type ValidationStates = Record<string, ValidationState>;
|
||||
@@ -119,6 +120,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
isValid: result.isValid,
|
||||
error: result.error,
|
||||
resolvedIp: result.resolvedIp,
|
||||
cdnProvider: result.cdnProvider,
|
||||
message: result.error && result.isValid ? result.error : undefined,
|
||||
},
|
||||
}));
|
||||
@@ -354,8 +356,9 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
) : validationState?.isValid ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 mr-1" />
|
||||
{validationState.message
|
||||
? "Behind Cloudflare"
|
||||
{validationState.message &&
|
||||
validationState.cdnProvider
|
||||
? `Behind ${validationState.cdnProvider}`
|
||||
: "DNS Valid"}
|
||||
</>
|
||||
) : validationState?.error ? (
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
currentBuildArgs !== (data?.buildArgs || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !hasChanges) {
|
||||
form.reset({
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
|
||||
@@ -44,8 +44,10 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
resolver: zodResolver(AddComposeFile),
|
||||
});
|
||||
|
||||
const composeFile = form.watch("composeFile");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !composeFile) {
|
||||
form.reset({
|
||||
composeFile: data.composeFile || "",
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ const AddProjectSchema = z.object({
|
||||
(name) => {
|
||||
const trimmedName = name.trim();
|
||||
const validNameRegex =
|
||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_-]*[\p{L}\p{N}_-]$/u;
|
||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_.-]*[\p{L}\p{N}_-]$/u;
|
||||
return validNameRegex.test(trimmedName);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,80 +1,89 @@
|
||||
export const extractExpirationDate = (certData: string): Date | null => {
|
||||
try {
|
||||
const match = certData.match(
|
||||
/-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/,
|
||||
);
|
||||
if (!match?.[1]) return null;
|
||||
|
||||
const base64Cert = match[1].replace(/\s/g, "");
|
||||
const binaryStr = window.atob(base64Cert);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
// Decode PEM base64 to DER binary
|
||||
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||
const binStr = atob(b64);
|
||||
const der = new Uint8Array(binStr.length);
|
||||
for (let i = 0; i < binStr.length; i++) {
|
||||
der[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
|
||||
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
|
||||
let dateFound = false;
|
||||
for (let i = 0; i < bytes.length - 2; i++) {
|
||||
// Look for sequence containing validity period (0x30)
|
||||
if (bytes[i] === 0x30) {
|
||||
// Check next bytes for UTCTime or GeneralizedTime
|
||||
let j = i + 1;
|
||||
while (j < bytes.length - 2) {
|
||||
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
|
||||
const dateType = bytes[j];
|
||||
const dateLength = bytes[j + 1];
|
||||
if (typeof dateLength === "undefined") break;
|
||||
let offset = 0;
|
||||
|
||||
if (!dateFound) {
|
||||
// Skip "not before" date
|
||||
dateFound = true;
|
||||
j += dateLength + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found "not after" date
|
||||
let dateStr = "";
|
||||
for (let k = 0; k < dateLength; k++) {
|
||||
const charCode = bytes[j + 2 + k];
|
||||
if (typeof charCode === "undefined") continue;
|
||||
dateStr += String.fromCharCode(charCode);
|
||||
}
|
||||
|
||||
if (dateType === 0x17) {
|
||||
// UTCTime (YYMMDDhhmmssZ)
|
||||
const year = Number.parseInt(dateStr.slice(0, 2));
|
||||
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
fullYear,
|
||||
Number.parseInt(dateStr.slice(2, 4)) - 1,
|
||||
Number.parseInt(dateStr.slice(4, 6)),
|
||||
Number.parseInt(dateStr.slice(6, 8)),
|
||||
Number.parseInt(dateStr.slice(8, 10)),
|
||||
Number.parseInt(dateStr.slice(10, 12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// GeneralizedTime (YYYYMMDDhhmmssZ)
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
Number.parseInt(dateStr.slice(0, 4)),
|
||||
Number.parseInt(dateStr.slice(4, 6)) - 1,
|
||||
Number.parseInt(dateStr.slice(6, 8)),
|
||||
Number.parseInt(dateStr.slice(8, 10)),
|
||||
Number.parseInt(dateStr.slice(10, 12)),
|
||||
Number.parseInt(dateStr.slice(12, 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
j++;
|
||||
// Helper: read ASN.1 length field
|
||||
function readLength(pos: number): { length: number; offset: number } {
|
||||
let len = der[pos++];
|
||||
if (len & 0x80) {
|
||||
const bytes = len & 0x7f;
|
||||
len = 0;
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
len = (len << 8) + der[pos++];
|
||||
}
|
||||
}
|
||||
return { length: len, offset: pos };
|
||||
}
|
||||
return null;
|
||||
|
||||
// Skip the outer certificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Skip tbsCertificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Check for optional version field (context-specific tag [0])
|
||||
if (der[offset] === 0xa0) {
|
||||
offset++;
|
||||
const versionLen = readLength(offset);
|
||||
offset = versionLen.offset + versionLen.length;
|
||||
}
|
||||
|
||||
// Skip serialNumber, signature, issuer
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (der[offset] !== 0x30 && der[offset] !== 0x02)
|
||||
throw new Error("Unexpected structure");
|
||||
offset++;
|
||||
const fieldLen = readLength(offset);
|
||||
offset = fieldLen.offset + fieldLen.length;
|
||||
}
|
||||
|
||||
// Validity sequence (notBefore and notAfter)
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected validity sequence");
|
||||
const validityLen = readLength(offset);
|
||||
offset = validityLen.offset;
|
||||
|
||||
// notBefore
|
||||
offset++;
|
||||
const notBeforeLen = readLength(offset);
|
||||
offset = notBeforeLen.offset + notBeforeLen.length;
|
||||
|
||||
// notAfter
|
||||
offset++;
|
||||
const notAfterLen = readLength(offset);
|
||||
const notAfterStr = new TextDecoder().decode(
|
||||
der.slice(notAfterLen.offset, notAfterLen.offset + notAfterLen.length),
|
||||
);
|
||||
|
||||
// Parse GeneralizedTime (15 chars) or UTCTime (13 chars)
|
||||
function parseTime(str: string): Date {
|
||||
if (str.length === 13) {
|
||||
// UTCTime YYMMDDhhmmssZ
|
||||
const year = parseInt(str.slice(0, 2), 10);
|
||||
const fullYear = year < 50 ? 2000 + year : 1900 + year;
|
||||
return new Date(
|
||||
`${fullYear}-${str.slice(2, 4)}-${str.slice(4, 6)}T${str.slice(6, 8)}:${str.slice(8, 10)}:${str.slice(10, 12)}Z`,
|
||||
);
|
||||
} else if (str.length === 15) {
|
||||
// GeneralizedTime YYYYMMDDhhmmssZ
|
||||
return new Date(
|
||||
`${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}T${str.slice(8, 10)}:${str.slice(10, 12)}:${str.slice(12, 14)}Z`,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Invalid ASN.1 time format");
|
||||
}
|
||||
}
|
||||
|
||||
return parseTime(notAfterStr);
|
||||
} catch (error) {
|
||||
console.error("Error parsing certificate:", error);
|
||||
return null;
|
||||
|
||||
@@ -41,6 +41,7 @@ const addInvitation = z.object({
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
notificationId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddInvitation = z.infer<typeof addInvitation>;
|
||||
@@ -49,6 +50,10 @@ export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
@@ -56,6 +61,7 @@ export const AddInvitation = () => {
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "member",
|
||||
notificationId: "",
|
||||
},
|
||||
resolver: zodResolver(addInvitation),
|
||||
});
|
||||
@@ -74,7 +80,20 @@ export const AddInvitation = () => {
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
if (!isCloud && data.notificationId) {
|
||||
await sendInvitation({
|
||||
invitationId: result.data.id,
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Invitation created and email sent");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
@@ -149,6 +168,47 @@ export const AddInvitation = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationId"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Email Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an email provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{emailProviders?.map((provider) => (
|
||||
<SelectItem
|
||||
key={provider.notificationId}
|
||||
value={provider.notificationId}
|
||||
>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
None
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the email provider to send the invitation
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
|
||||
3
apps/dokploy/drizzle/0093_nice_gorilla_man.sql
Normal file
3
apps/dokploy/drizzle/0093_nice_gorilla_man.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "user_temp" ALTER COLUMN "logCleanupCron" SET DEFAULT '0 0 * * *';
|
||||
|
||||
UPDATE "user_temp" SET "logCleanupCron" = '0 0 * * *' WHERE "logCleanupCron" IS NULL;
|
||||
5718
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
5718
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -652,6 +652,13 @@
|
||||
"when": 1747713229160,
|
||||
"tag": "0092_stiff_the_watchers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 93,
|
||||
"version": "7",
|
||||
"when": 1750397258622,
|
||||
"tag": "0093_nice_gorilla_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -74,12 +74,7 @@ const Service = (
|
||||
}
|
||||
}, [router.query.tab]);
|
||||
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{
|
||||
refetchInterval: 5000,
|
||||
},
|
||||
);
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
findPostgresByBackupId,
|
||||
findPostgresById,
|
||||
findServerById,
|
||||
keepLatestNBackups,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runMariadbBackup,
|
||||
@@ -197,6 +198,8 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||
await runPostgresBackup(postgres, backup);
|
||||
|
||||
await keepLatestNBackups(backup, postgres?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -217,6 +220,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
await keepLatestNBackups(backup, mysql?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -233,6 +237,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -249,6 +254,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const compose = await findComposeByBackupId(backup.backupId);
|
||||
await runComposeBackup(compose, backup);
|
||||
await keepLatestNBackups(backup, compose?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -265,6 +271,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mongo = await findMongoByBackupId(backup.backupId);
|
||||
await runMongoBackup(mongo, backup);
|
||||
await keepLatestNBackups(backup, mongo?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -496,7 +496,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return compose;
|
||||
}),
|
||||
|
||||
templates: publicProcedure
|
||||
|
||||
@@ -446,4 +446,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -825,6 +825,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
if (input.cronExpression) {
|
||||
return startLogCleanup(input.cronExpression);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
@@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return organizations.length;
|
||||
}),
|
||||
sendInvitation: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
notificationId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
|
||||
const email = notification.email;
|
||||
|
||||
const currentInvitation = await db.query.invitation.findFirst({
|
||||
where: eq(invitation.id, input.invitationId),
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Email notification not found",
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await findAdmin();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: admin.user.host;
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
try {
|
||||
await sendEmailNotification(
|
||||
{
|
||||
...email,
|
||||
toAddresses: [currentInvitation?.email || ""],
|
||||
},
|
||||
"Invitation to join organization",
|
||||
`
|
||||
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||
`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
return inviteLink;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export const users_temp = pgTable("user_temp", {
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron"),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
|
||||
@@ -18,9 +18,6 @@ const { handler, api } = betterAuth({
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
|
||||
634
packages/server/src/services/cdn.ts
Normal file
634
packages/server/src/services/cdn.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
// CDN Provider Interface
|
||||
export interface CDNProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
checkIp: (ip: string) => boolean;
|
||||
warningMessage: string;
|
||||
}
|
||||
|
||||
const isIPInCIDR = (ip: string, cidr: string): boolean => {
|
||||
const [network, prefixLength] = cidr.split("/");
|
||||
if (!network || !prefixLength) return false;
|
||||
const prefix = Number.parseInt(prefixLength, 10);
|
||||
|
||||
// Convert IP addresses to 32-bit integers
|
||||
const ipToInt = (ipStr: string): number => {
|
||||
return (
|
||||
ipStr
|
||||
.split(".")
|
||||
.reduce((acc, octet) => (acc << 8) + Number.parseInt(octet, 10), 0) >>>
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const ipInt = ipToInt(ip);
|
||||
const networkInt = ipToInt(network);
|
||||
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (networkInt & mask);
|
||||
};
|
||||
|
||||
// Cloudflare IP ranges
|
||||
// https://www.cloudflare.com/ips-v4
|
||||
const CLOUDFLARE_IP_RANGES = [
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
];
|
||||
|
||||
// Fastly IP ranges
|
||||
// https://api.fastly.com/public-ip-list
|
||||
const FASTLY_IP_RANGES = [
|
||||
"23.235.32.0/20",
|
||||
"43.249.72.0/22",
|
||||
"103.244.50.0/24",
|
||||
"103.245.222.0/23",
|
||||
"103.245.224.0/24",
|
||||
"104.156.80.0/20",
|
||||
"140.248.64.0/18",
|
||||
"140.248.128.0/17",
|
||||
"146.75.0.0/17",
|
||||
"151.101.0.0/16",
|
||||
"157.52.64.0/18",
|
||||
"167.82.0.0/17",
|
||||
"167.82.128.0/20",
|
||||
"167.82.160.0/20",
|
||||
"167.82.224.0/20",
|
||||
"172.111.64.0/18",
|
||||
"185.31.16.0/22",
|
||||
"199.27.72.0/21",
|
||||
"199.232.0.0/16",
|
||||
];
|
||||
|
||||
// Bunny CDN IP addresses
|
||||
// https://bunnycdn.com/api/system/edgeserverlist
|
||||
const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
"185.93.1.243",
|
||||
"156.146.40.49",
|
||||
"185.59.220.199",
|
||||
"185.59.220.198",
|
||||
"195.181.166.158",
|
||||
"185.180.12.68",
|
||||
"138.199.24.209",
|
||||
"138.199.24.211",
|
||||
"79.127.216.111",
|
||||
"79.127.216.112",
|
||||
"89.187.169.47",
|
||||
"138.199.24.218",
|
||||
"185.40.106.117",
|
||||
"200.25.45.4",
|
||||
"200.25.57.5",
|
||||
"193.162.131.1",
|
||||
"200.25.11.8",
|
||||
"200.25.53.5",
|
||||
"200.25.13.98",
|
||||
"41.242.2.18",
|
||||
"200.25.62.5",
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
"138.199.37.231",
|
||||
"138.199.37.230",
|
||||
"138.199.37.229",
|
||||
"138.199.46.69",
|
||||
"138.199.46.68",
|
||||
"138.199.46.67",
|
||||
"185.93.1.246",
|
||||
"138.199.37.232",
|
||||
"195.181.163.196",
|
||||
"107.182.163.162",
|
||||
"195.181.163.195",
|
||||
"84.17.46.53",
|
||||
"212.102.40.114",
|
||||
"84.17.46.54",
|
||||
"138.199.40.58",
|
||||
"143.244.38.134",
|
||||
"143.244.38.136",
|
||||
"185.152.64.17",
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
"143.244.49.179",
|
||||
"143.244.49.180",
|
||||
"138.199.9.104",
|
||||
"185.152.66.243",
|
||||
"143.244.49.178",
|
||||
"169.150.221.147",
|
||||
"200.25.18.73",
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
"185.93.1.250",
|
||||
"169.150.215.115",
|
||||
"209.177.87.197",
|
||||
"156.146.56.162",
|
||||
"156.146.56.161",
|
||||
"185.93.2.246",
|
||||
"185.93.2.245",
|
||||
"212.102.40.113",
|
||||
"185.93.2.244",
|
||||
"143.244.50.82",
|
||||
"143.244.50.83",
|
||||
"156.146.56.163",
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
"195.181.163.202",
|
||||
"156.146.56.169",
|
||||
"156.146.56.170",
|
||||
"156.146.56.166",
|
||||
"156.146.56.171",
|
||||
"169.150.207.210",
|
||||
"156.146.56.167",
|
||||
"143.244.50.84",
|
||||
"143.244.50.85",
|
||||
"143.244.50.86",
|
||||
"143.244.50.87",
|
||||
"156.146.56.168",
|
||||
"169.150.207.211",
|
||||
"212.102.50.59",
|
||||
"146.185.248.15",
|
||||
"143.244.50.90",
|
||||
"143.244.50.91",
|
||||
"143.244.50.88",
|
||||
"143.244.50.209",
|
||||
"143.244.50.213",
|
||||
"143.244.50.214",
|
||||
"143.244.49.183",
|
||||
"143.244.50.89",
|
||||
"143.244.50.210",
|
||||
"143.244.50.211",
|
||||
"143.244.50.212",
|
||||
"5.42.206.66",
|
||||
"94.46.27.186",
|
||||
"169.150.207.213",
|
||||
"169.150.207.214",
|
||||
"169.150.207.215",
|
||||
"169.150.207.212",
|
||||
"169.150.219.114",
|
||||
"169.150.202.210",
|
||||
"169.150.242.193",
|
||||
"185.93.1.251",
|
||||
"169.150.207.216",
|
||||
"169.150.207.217",
|
||||
"169.150.238.19",
|
||||
"102.219.126.20",
|
||||
"156.59.66.182",
|
||||
"122.10.251.130",
|
||||
"185.24.11.18",
|
||||
"138.199.36.7",
|
||||
"138.199.36.8",
|
||||
"138.199.36.9",
|
||||
"138.199.36.10",
|
||||
"138.199.36.11",
|
||||
"138.199.37.225",
|
||||
"84.17.46.49",
|
||||
"84.17.37.217",
|
||||
"169.150.225.35",
|
||||
"169.150.225.36",
|
||||
"169.150.225.37",
|
||||
"169.150.225.38",
|
||||
"169.150.225.39",
|
||||
"169.150.225.34",
|
||||
"169.150.236.97",
|
||||
"169.150.236.98",
|
||||
"169.150.236.99",
|
||||
"169.150.236.100",
|
||||
"93.189.63.149",
|
||||
"143.244.56.49",
|
||||
"143.244.56.50",
|
||||
"143.244.56.51",
|
||||
"169.150.247.40",
|
||||
"169.150.247.33",
|
||||
"169.150.247.34",
|
||||
"169.150.247.35",
|
||||
"169.150.247.36",
|
||||
"169.150.247.37",
|
||||
"169.150.247.38",
|
||||
"169.150.247.39",
|
||||
"38.142.94.218",
|
||||
"87.249.137.52",
|
||||
"38.104.169.186",
|
||||
"66.181.163.74",
|
||||
"84.17.38.227",
|
||||
"84.17.38.228",
|
||||
"84.17.38.229",
|
||||
"84.17.38.230",
|
||||
"84.17.38.231",
|
||||
"84.17.38.232",
|
||||
"169.150.225.41",
|
||||
"169.150.225.42",
|
||||
"169.150.249.162",
|
||||
"169.150.249.163",
|
||||
"169.150.249.164",
|
||||
"169.150.249.165",
|
||||
"169.150.249.166",
|
||||
"169.150.249.167",
|
||||
"169.150.249.168",
|
||||
"169.150.249.169",
|
||||
"185.131.64.124",
|
||||
"103.112.0.22",
|
||||
"37.236.234.2",
|
||||
"169.150.252.209",
|
||||
"212.102.46.118",
|
||||
"192.169.120.162",
|
||||
"93.180.217.214",
|
||||
"37.19.203.178",
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
"103.216.220.9",
|
||||
"169.150.225.40",
|
||||
"212.102.50.49",
|
||||
"212.102.50.52",
|
||||
"109.61.83.242",
|
||||
"109.61.83.243",
|
||||
"212.102.50.50",
|
||||
"169.150.225.43",
|
||||
"45.125.247.57",
|
||||
"103.235.199.170",
|
||||
"128.1.35.170",
|
||||
"38.32.110.58",
|
||||
"169.150.220.228",
|
||||
"169.150.220.229",
|
||||
"169.150.220.230",
|
||||
"169.150.220.231",
|
||||
"138.199.4.179",
|
||||
"207.211.214.145",
|
||||
"109.61.86.193",
|
||||
"103.214.20.95",
|
||||
"178.175.134.51",
|
||||
"138.199.4.178",
|
||||
"172.255.253.140",
|
||||
"185.24.11.19",
|
||||
"109.61.83.244",
|
||||
"109.61.83.245",
|
||||
"84.17.38.250",
|
||||
"84.17.38.251",
|
||||
"146.59.69.202",
|
||||
"146.70.80.218",
|
||||
"200.25.80.74",
|
||||
"79.127.213.214",
|
||||
"79.127.213.215",
|
||||
"79.127.213.216",
|
||||
"79.127.213.217",
|
||||
"195.69.140.112",
|
||||
"109.61.83.247",
|
||||
"109.61.83.246",
|
||||
"185.93.2.248",
|
||||
"109.61.83.249",
|
||||
"109.61.83.250",
|
||||
"109.61.83.251",
|
||||
"46.199.75.115",
|
||||
"141.164.35.160",
|
||||
"109.61.83.97",
|
||||
"109.61.83.98",
|
||||
"109.61.83.99",
|
||||
"129.227.179.18",
|
||||
"185.180.14.250",
|
||||
"152.89.160.26",
|
||||
"5.189.202.62",
|
||||
"98.98.242.142",
|
||||
"156.59.92.126",
|
||||
"84.17.59.117",
|
||||
"79.127.216.66",
|
||||
"79.127.204.113",
|
||||
"79.127.237.132",
|
||||
"169.150.236.104",
|
||||
"169.150.236.105",
|
||||
"37.27.135.61",
|
||||
"158.51.123.205",
|
||||
"156.146.43.70",
|
||||
"156.146.43.71",
|
||||
"156.146.43.72",
|
||||
"180.149.231.175",
|
||||
"185.93.2.243",
|
||||
"143.244.56.52",
|
||||
"143.244.56.53",
|
||||
"143.244.56.54",
|
||||
"143.244.56.55",
|
||||
"143.244.56.56",
|
||||
"143.244.56.57",
|
||||
"143.244.56.58",
|
||||
"144.76.236.44",
|
||||
"88.198.57.50",
|
||||
"78.46.69.199",
|
||||
"136.243.16.49",
|
||||
"138.201.86.122",
|
||||
"136.243.42.90",
|
||||
"88.99.95.221",
|
||||
"178.63.2.112",
|
||||
"5.9.98.45",
|
||||
"136.243.42.10",
|
||||
"169.150.236.106",
|
||||
"169.150.236.107",
|
||||
"185.93.1.242",
|
||||
"185.93.1.245",
|
||||
"143.244.60.193",
|
||||
"195.181.163.194",
|
||||
"79.127.188.193",
|
||||
"79.127.188.196",
|
||||
"79.127.188.194",
|
||||
"79.127.188.195",
|
||||
"104.166.144.106",
|
||||
"156.59.126.78",
|
||||
"185.135.85.154",
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
"144.76.65.213",
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
"188.40.126.227",
|
||||
"88.99.26.189",
|
||||
"168.119.39.238",
|
||||
"88.99.26.97",
|
||||
"168.119.12.188",
|
||||
"176.9.139.55",
|
||||
"142.132.223.79",
|
||||
"142.132.223.80",
|
||||
"142.132.223.81",
|
||||
"46.4.116.17",
|
||||
"46.4.119.81",
|
||||
"167.235.114.167",
|
||||
"159.69.68.171",
|
||||
"178.63.21.52",
|
||||
"46.4.120.152",
|
||||
"116.202.80.247",
|
||||
"5.9.71.119",
|
||||
"195.201.11.156",
|
||||
"78.46.123.17",
|
||||
"46.4.113.143",
|
||||
"136.243.2.236",
|
||||
"195.201.81.217",
|
||||
"148.251.42.123",
|
||||
"94.130.68.122",
|
||||
"88.198.22.103",
|
||||
"46.4.102.90",
|
||||
"157.90.180.205",
|
||||
"162.55.135.11",
|
||||
"195.201.109.59",
|
||||
"148.251.41.244",
|
||||
"116.202.235.16",
|
||||
"128.140.70.141",
|
||||
"78.46.74.86",
|
||||
"78.46.74.85",
|
||||
"178.63.41.242",
|
||||
"178.63.41.247",
|
||||
"178.63.41.234",
|
||||
"104.237.53.74",
|
||||
"104.237.54.154",
|
||||
"104.237.51.58",
|
||||
"64.185.235.90",
|
||||
"64.185.234.114",
|
||||
"64.185.232.194",
|
||||
"64.185.232.178",
|
||||
"64.185.232.82",
|
||||
"103.60.15.169",
|
||||
"103.60.15.170",
|
||||
"103.60.15.171",
|
||||
"103.60.15.172",
|
||||
"103.60.15.173",
|
||||
"103.60.15.162",
|
||||
"103.60.15.163",
|
||||
"103.60.15.164",
|
||||
"103.60.15.165",
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
"109.248.43.163",
|
||||
"109.248.43.164",
|
||||
"109.248.43.165",
|
||||
"49.12.71.27",
|
||||
"49.12.0.158",
|
||||
"78.47.94.156",
|
||||
"109.248.43.159",
|
||||
"109.248.43.160",
|
||||
"109.248.43.208",
|
||||
"109.248.43.179",
|
||||
"109.248.43.232",
|
||||
"109.248.43.231",
|
||||
"109.248.43.241",
|
||||
"109.248.43.236",
|
||||
"109.248.43.240",
|
||||
"109.248.43.103",
|
||||
"116.202.118.194",
|
||||
"116.202.80.29",
|
||||
"159.69.57.80",
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"89.187.179.7",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
"185.152.64.19",
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
"84.17.38.226",
|
||||
"84.17.38.225",
|
||||
"37.19.222.248",
|
||||
"37.19.222.249",
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
"169.150.247.179",
|
||||
"169.150.247.180",
|
||||
"169.150.247.181",
|
||||
"169.150.247.182",
|
||||
"169.150.247.183",
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
"109.61.89.58",
|
||||
"109.61.83.241",
|
||||
"84.17.38.243",
|
||||
"84.17.38.244",
|
||||
"84.17.38.246",
|
||||
"84.17.38.247",
|
||||
"84.17.38.245",
|
||||
"143.244.38.129",
|
||||
"84.17.38.248",
|
||||
"89.187.176.34",
|
||||
"185.152.64.23",
|
||||
"79.127.213.209",
|
||||
"79.127.213.210",
|
||||
"84.17.37.209",
|
||||
"156.146.43.68",
|
||||
"185.93.3.243",
|
||||
"79.127.219.198",
|
||||
"138.199.33.57",
|
||||
"79.127.242.89",
|
||||
"138.199.4.136",
|
||||
"169.150.220.235",
|
||||
"138.199.4.129",
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
"212.104.158.20",
|
||||
"212.104.158.21",
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
]);
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
displayName: "Cloudflare",
|
||||
checkIp: (ip: string) =>
|
||||
CLOUDFLARE_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
},
|
||||
{
|
||||
name: "bunnycdn",
|
||||
displayName: "Bunny CDN",
|
||||
checkIp: (ip: string) => BUNNY_CDN_IPS.has(ip),
|
||||
warningMessage:
|
||||
"Domain is behind Bunny CDN - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "fastly",
|
||||
displayName: "Fastly",
|
||||
checkIp: (ip: string) =>
|
||||
FASTLY_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
return CDN_PROVIDERS.find((provider) => provider.checkIp(ip)) || null;
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { eq } from "drizzle-orm";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
@@ -142,28 +143,6 @@ export const getDomainHost = (domain: Domain) => {
|
||||
|
||||
const resolveDns = promisify(dns.resolve4);
|
||||
|
||||
// Cloudflare IP ranges (simplified - these are some common ones)
|
||||
const CLOUDFLARE_IPS = [
|
||||
"172.67.",
|
||||
"104.21.",
|
||||
"104.16.",
|
||||
"104.17.",
|
||||
"104.18.",
|
||||
"104.19.",
|
||||
"104.20.",
|
||||
"104.22.",
|
||||
"104.23.",
|
||||
"104.24.",
|
||||
"104.25.",
|
||||
"104.26.",
|
||||
"104.27.",
|
||||
"104.28.",
|
||||
];
|
||||
|
||||
const isCloudflareIp = (ip: string) => {
|
||||
return CLOUDFLARE_IPS.some((range) => ip.startsWith(range));
|
||||
};
|
||||
|
||||
export const validateDomain = async (
|
||||
domain: string,
|
||||
expectedIp?: string,
|
||||
@@ -172,6 +151,7 @@ export const validateDomain = async (
|
||||
resolvedIp?: string;
|
||||
error?: string;
|
||||
isCloudflare?: boolean;
|
||||
cdnProvider?: string;
|
||||
}> => {
|
||||
try {
|
||||
// Remove protocol and path if present
|
||||
@@ -182,17 +162,18 @@ export const validateDomain = async (
|
||||
|
||||
const resolvedIps = ips.map((ip) => ip.toString());
|
||||
|
||||
// Check if it's a Cloudflare IP
|
||||
const behindCloudflare = ips.some((ip) => isCloudflareIp(ip));
|
||||
// Check if any IP belongs to a CDN provider
|
||||
const cdnProvider = ips
|
||||
.map((ip) => detectCDNProvider(ip))
|
||||
.find((provider) => provider !== null);
|
||||
|
||||
// If behind Cloudflare, we consider it valid but inform the user
|
||||
if (behindCloudflare) {
|
||||
// If behind a CDN, we consider it valid but inform the user
|
||||
if (cdnProvider) {
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
isCloudflare: true,
|
||||
error:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
cdnProvider: cdnProvider.displayName,
|
||||
error: cdnProvider.warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully after retry");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,7 +37,8 @@ export const startLogCleanup = async (
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error("Error starting log cleanup:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,6 +88,7 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
@@ -81,10 +84,10 @@ export const buildRailpack = async (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const exportEnvs = [];
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
exportEnvs.push(`export ${key}=${value}`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const parseEnvironmentKeyValuePair = (
|
||||
pair: string,
|
||||
): [string, string] => {
|
||||
const [key, ...valueParts] = pair.split("=");
|
||||
if (!key || !valueParts.length) {
|
||||
throw new Error(`Invalid environment variable pair: ${pair}`);
|
||||
}
|
||||
|
||||
return [key, valueParts.join("")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
|
||||
const jsonObject: Record<string, string> = {};
|
||||
|
||||
for (const pair of envs) {
|
||||
const [key, value] = pair.split("=");
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
jsonObject[key] = value;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const cloneBitbucketRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -155,7 +155,7 @@ export const getGiteaCloneCommand = async (
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
@@ -232,7 +232,7 @@ export const cloneGiteaRepository = async (
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -149,7 +149,7 @@ export const cloneGithubRepository = async ({
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -132,7 +132,7 @@ export const cloneGitlabRepository = async (
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
@@ -152,7 +152,7 @@ export const cloneGitlabRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
Reference in New Issue
Block a user