Compare commits

...

66 Commits

Author SHA1 Message Date
Mauricio Siu
7fe163dd33 Merge pull request #1913 from Dokploy/feat/add-appname-compose
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add appname compose
2025-05-17 15:37:22 -06:00
Mauricio Siu
19b56771b8 style: update styling for environment display and increase scroll area height in import component 2025-05-17 15:28:52 -06:00
Mauricio Siu
cff01ed438 refactor: modify template processing to include APP_NAME variable in configuration 2025-05-17 15:27:42 -06:00
Mauricio Siu
10fa3c8cf1 fix: update environment file generation to include APP_NAME variable 2025-05-17 15:18:31 -06:00
Mauricio Siu
6c5497ed21 Merge pull request #1912 from Dokploy/1873-duplicate-clones-the-project-not-the-service
feat: enhance project duplication functionality with options for new …
2025-05-17 14:35:17 -06:00
Mauricio Siu
380656efee feat: enhance project duplication functionality with options for new or same project 2025-05-17 14:33:07 -06:00
Mauricio Siu
c64d2245ce Merge pull request #1897 from enie123/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
build: update nixpacks to 1.39.0
2025-05-17 03:54:11 -06:00
Mauricio Siu
a985998b93 feat: add VPS provider recommendations and alerts in server settings 2025-05-17 03:53:37 -06:00
Mauricio Siu
4f3ba16dfa chore: bump version to v0.22.5 in package.json 2025-05-17 03:34:31 -06:00
Mauricio Siu
6c788429f1 Merge pull request #1910 from Dokploy/1894-gitlab-self-hosted-cannot-find-all-repository
refactor: streamline GitLab repository fetching by introducing valida…
2025-05-17 03:02:26 -06:00
Mauricio Siu
3176a9d7e3 refactor: streamline GitLab repository fetching by introducing validateGitlabProvider function for improved error handling and pagination 2025-05-17 02:51:40 -06:00
Mauricio Siu
94a6a9587e Merge pull request #1872 from IPdotSetAF/git-lfs-not-supported
fix: installed git-lfs in docker image
2025-05-17 02:09:03 -06:00
Mauricio Siu
911681f389 fix: add git-lfs installation to various OS setups in server-setup script 2025-05-17 02:03:04 -06:00
Mauricio Siu
5992688e85 Merge pull request #1909 from Dokploy/1868-backups-failing-due-to-a-directory-not-empty-error
refactor: improve cleanup process in web server backup utility to han…
2025-05-17 00:21:58 -06:00
Mauricio Siu
425061e481 refactor: improve cleanup process in web server backup utility to handle errors during temporary directory removal 2025-05-17 00:20:54 -06:00
Mauricio Siu
08c0bf8a21 Merge pull request #1908 from Dokploy/1863-pg_dump-backup-fails-stdout-maxbuffer-length-exceeded
refactor: update database backup process in web server utility to use…
2025-05-17 00:14:59 -06:00
Mauricio Siu
64a2c9e0a1 refactor: update database backup process in web server utility to use temporary file in container 2025-05-17 00:13:43 -06:00
Mauricio Siu
21e46f5382 Merge pull request #1907 from Dokploy/1878-dokploy-application-settings-not-linked-resulting-in-progress-lost-on-save
fix: update dependencies in save provider components to use optional …
2025-05-16 23:23:20 -06:00
autofix-ci[bot]
52b2158309 [autofix.ci] apply automated fixes 2025-05-17 05:22:55 +00:00
Mauricio Siu
178d84d438 fix: update dependencies in save provider components to use optional chaining for applicationId and composeId 2025-05-16 23:22:26 -06:00
Mauricio Siu
80016b57a8 Merge pull request #1906 from Dokploy/1888-docker-compose-preview-is-null
feat(ui): add loading state and no data message to converted compose …
2025-05-16 23:16:29 -06:00
Mauricio Siu
b4b2d12f6e feat(ui): add loading state and no data message to converted compose display 2025-05-16 23:16:02 -06:00
Mauricio Siu
294378d95b Merge pull request #1886 from oshanavishkapiries/canary
fix: Submit Log in issue on Github
2025-05-16 23:03:50 -06:00
Mauricio Siu
c52812f9d3 Merge pull request #1903 from yergom/fix/watch-path-wording
fix: more informative placeholder for watch path
2025-05-16 23:03:13 -06:00
Mauricio Siu
82f7c5d5f3 Merge pull request #1904 from darena-patrick/fix/compose-deploy-url
fix: Missing `/compose` in auto-deploy URL
2025-05-16 23:00:47 -06:00
Mauricio Siu
3d2ae52259 Merge pull request #1891 from nktnet1/fix-domain-responsiveness
Fix domain responsiveness
2025-05-16 22:59:41 -06:00
Patrick Schiess
bf115c7895 fix: Missing /compose in auto-deploy URL 2025-05-16 16:12:09 -06:00
yergom
c2c29dbaba fix: more informative placeholder for watch path 2025-05-16 13:25:28 +00:00
Eric Nie
d4032f34bf build: update nixpacks to 1.39.0 2025-05-15 00:45:48 +07:00
Tam Nguyen
136570b36c fix(ui): compose grid responsiveness starts from xl instead of lg 2025-05-13 16:01:25 +10:00
Tam Nguyen
7d0075c230 fix(ui): domain responsiveness with screen width 2025-05-13 15:43:52 +10:00
Tam Nguyen
19b4edee8d refactor: remove redundant class bg-card due to bg-transparent set later 2025-05-13 15:15:54 +10:00
153918928+oshanavishkapiries@users.noreply.github.com
7f04eb856e fix: Submit Log in issue on Github 2025-05-13 01:56:55 +05:30
IPdotSetAF
5156b45ffc fix: installed git-lfs in docker image 2025-05-11 10:36:52 +03:30
Mauricio Siu
80e6f21840 chore: bump version to v0.22.4 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-10 20:54:36 -06:00
Mauricio Siu
5b519151e8 refactor: streamline Remove Invitation dropdown menu item in ShowInvitations component 2025-05-10 20:40:11 -06:00
Mauricio Siu
aa475e6123 Merge pull request #1860 from yusoofsh/pgrestore-no-owner
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Add no owner options to pg_restore
2025-05-10 15:22:41 -06:00
Mauricio Siu
66756c34fe chore: update AmericanCloud logo in README to PNG format and adjust height
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
2025-05-10 03:30:01 -06:00
Mauricio Siu
946a5739dc chore: replace AmericanCloud SVG logo with PNG format 2025-05-10 03:29:38 -06:00
Mauricio Siu
6c817a9e5d feat: add AmericanCloud sponsorship to README and include SVG logo 2025-05-10 03:28:19 -06:00
Mauricio Siu
6aea937e86 chore: remove bun.lock file to clean up unused lockfile 2025-05-10 03:10:49 -06:00
Mauricio Siu
19612d4b66 Revert "refactor: remove unused volume suffix function from collision utility"
This reverts commit 47dd003461.
2025-05-10 02:58:01 -06:00
Mauricio Siu
47dd003461 refactor: remove unused volume suffix function from collision utility 2025-05-10 02:57:21 -06:00
Mauricio Siu
def99225fc Merge pull request #1865 from Dokploy/fix/isolated-docker-stack
fix: update Docker network creation command to support overlay driver in docker stack deployments
2025-05-10 02:33:04 -06:00
Mauricio Siu
32405fc61a fix: update Docker network creation command to support overlay driver for stack deployments 2025-05-10 02:13:57 -06:00
Mauricio Siu
25e1a9af57 Merge pull request #1859 from yusoofsh/fix-swarm-database-backup-restore
Fix container not found upon backup/restore on compose stack
2025-05-10 01:45:12 -06:00
Mauricio Siu
1fcb1f2c5e fix: update Docker command filter for service name in backup utility 2025-05-10 01:43:57 -06:00
Mauricio Siu
fdaba7e752 Merge pull request #1864 from Dokploy/fix/404-unauthorized-endpoints
chore: update better-auth to v1.2.8-beta.7 in package.json and pnpm-l…
2025-05-10 01:37:51 -06:00
Mauricio Siu
c1640cba29 chore: update better-auth to v1.2.8-beta.7 in package.json and pnpm-lock.yaml 2025-05-10 01:30:03 -06:00
Yusoof Moh
3bd54ff61e fix: add no owner options to pg_restore 2025-05-10 00:12:33 +07:00
Yusoof Moh
5853d18bc1 fix: container not found upon backup/restore on compose stack 2025-05-09 23:42:34 +07:00
Mauricio Siu
f575317906 Merge pull request #1848 from Dokploy/feat/add-builders-alert
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
feat: add alert block to ShowBuildChooseForm for resource usage guidance
2025-05-06 23:37:24 -06:00
Mauricio Siu
e6028e73ac feat: add alert block to ShowBuildChooseForm for resource usage guidance 2025-05-06 23:37:05 -06:00
Mauricio Siu
bcbed151e8 Merge pull request #1841 from MauruschatM/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
update railpack to 0.0.64
2025-05-06 23:22:27 -06:00
Mauricio Siu
c708f7ba62 Update version in package.json to v0.22.3 2025-05-06 23:13:42 -06:00
Mauricio Siu
95a538f261 Merge pull request #1846 from Dokploy/fix/dockerfile-env-vars
fix: wrap build arguments in single quotes for Docker command
2025-05-06 23:12:35 -06:00
Mauricio Siu
f854457d69 fix: wrap build arguments in single quotes for Docker command 2025-05-06 23:11:37 -06:00
Mauricio Siu
cd998c37f1 refactor: update railpack 2025-05-06 23:04:58 -06:00
Mauricio Siu
d46a61098b Merge pull request #1840 from Smip/canary
fix: use root password instead of user one
2025-05-06 22:58:16 -06:00
Moritz Mauruschat
8f14d854a0 Update railpack to 0.064 2025-05-06 13:32:16 +02:00
Aleksandr Sokolov
388399b370 fix: use root password instead of user one 2025-05-06 13:02:26 +02:00
Mauricio Siu
a8b4bb9c41 Merge pull request #1835 from Dokploy/feat/add-impersionation-cloud
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add impersionation cloud
2025-05-06 02:39:24 -06:00
Mauricio Siu
ebc8c2f73d Add default user role and impersonation settings in tests
- Updated the baseAdmin user object in the update-server-config test to include default values for allowImpersonation and role, ensuring comprehensive test coverage for user management features.
2025-05-06 02:37:41 -06:00
Mauricio Siu
1227d2b5fc Update version in package.json to v0.22.2 2025-05-06 02:35:08 -06:00
Mauricio Siu
314438b84c Enhance impersonation functionality and user management
- Updated the ImpersonationBar component to fetch users dynamically and handle impersonation actions more efficiently.
- Refactored the ProfileForm to set the allowImpersonation value directly, improving form handling.
- Modified the DashboardLayout to conditionally render the ImpersonationBar based on user permissions and cloud settings.
- Added a new role column to the user_temp table to support user role management.
- Updated API routes to include checks for root access and improved user listing functionality.
2025-05-06 02:32:08 -06:00
Mauricio Siu
cc5574e08a Add impersonation feature to user management
- Introduced an ImpersonationBar component for admin users to impersonate other users, enhancing user management capabilities.
- Updated the ProfileForm to include an option for allowing impersonation, with a description for clarity.
- Modified the DashboardLayout to conditionally display the impersonation bar based on user roles and cloud settings.
- Added database schema changes to support the new impersonation feature, including a new column for allowImpersonation in the user table.
- Implemented necessary API updates to handle impersonation actions and user data retrieval.
2025-05-06 01:46:20 -06:00
54 changed files with 12467 additions and 251 deletions

BIN
.github/sponsors/american-cloud.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -49,18 +49,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0
ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \
&& pnpm install -g tsx
# Install Railpack
ARG RAILPACK_VERSION=0.0.37
ARG RAILPACK_VERSION=0.0.64
RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@@ -91,8 +91,19 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
</div>
### Elite Contributors 🥈
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
</a>
</div>
<!-- Elite Contributors 🥈 -->
<!-- Add Elite Contributors here -->
### Supporting Members 🥉
@@ -104,6 +115,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div>

View File

@@ -8,7 +8,7 @@ COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y python3 make g++ git git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

View File

@@ -16,6 +16,8 @@ import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = {
https: false,
enablePaidFeatures: false,
allowImpersonation: false,
role: "user",
metricsConfig: {
containers: {
refreshRate: 20,

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => (
<div
key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm"
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
>
{env}
</div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[25vh] pr-4">
<ScrollArea className="h-[45vh] pr-4">
<CodeEditor
language="yaml"
value={selectedMount?.content || ""}

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -200,6 +201,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
</CardHeader>
<CardContent>
<Form {...form}>
<AlertBlock>
Builders can consume significant memory and CPU resources
(recommended: 4+ GB RAM and 2+ CPU cores). For production
environments, please review our{" "}
<a
href="https://docs.dokploy.com/docs/core/applications/going-production"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
Production Guide
</a>{" "}
for best practices and optimization recommendations. Builders are
suitable for development and prototyping purposes when you have
sufficient resources available.
</AlertBlock>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 p-2"

View File

@@ -86,7 +86,7 @@ export const ShowDeployments = ({
<span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground">
{`${url}/api/deploy/${refreshToken}`}
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
</span>
{(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} />

View File

@@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => {
return (
<Card
key={item.domainId}
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent h-fit"
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
>
<CardContent className="p-6">
<div className="flex flex-col gap-4">
{/* Service & Domain Info */}
<div className="flex items-start justify-between">
<div className="flex flex-col gap-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4" />
</Link>
</div>
<div className="flex gap-2">
<div className="flex items-center justify-between flex-wrap gap-y-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<div className="flex gap-2 flex-wrap">
{!item.host.includes("traefik.me") && (
<DnsHelperModal
domain={{
@@ -266,6 +255,16 @@ export const ShowDomains = ({ id, type }: Props) => {
</DialogAction>
</div>
</div>
<div className="w-full break-all">
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4 min-w-4" />
</Link>
</div>
{/* Domain Details */}
<div className="flex flex-wrap gap-3">

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
registryURL: data.registryUrl || "",
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({

View File

@@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -470,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -10,7 +10,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>
{isLoading ? (
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
</div>
) : compose?.length === 5 ? (
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-muted-foreground">
No converted compose data available.
</span>
</div>
) : (
<>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</>
)}
</DialogContent>
</Dialog>
);

View File

@@ -0,0 +1,454 @@
"use client";
import { authClient } from "@/lib/auth-client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
CheckIcon,
ChevronsUpDown,
Settings2,
UserIcon,
XIcon,
Shield,
Calendar,
Key,
Copy,
Fingerprint,
Building2,
CreditCard,
Server,
} from "lucide-react";
import { toast } from "sonner";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Logo } from "@/components/shared/logo";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
import copy from "copy-to-clipboard";
import { api } from "@/utils/api";
type User = typeof authClient.$Infer.Session.user;
export const ImpersonationBar = () => {
const [users, setUsers] = useState<User[]>([]);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isImpersonating, setIsImpersonating] = useState(false);
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showBar, setShowBar] = useState(false);
const { data } = api.user.get.useQuery();
const fetchUsers = async (search?: string) => {
try {
const session = await authClient.getSession();
if (session?.data?.session?.impersonatedBy) {
return;
}
setIsLoading(true);
const response = await authClient.admin.listUsers({
query: {
limit: 30,
...(search && {
searchField: "email",
searchOperator: "contains",
searchValue: search,
}),
},
});
const filteredUsers = response.data?.users.filter(
// @ts-ignore
(user) => user.allowImpersonation && data?.user?.email !== user.email,
);
if (!response.error) {
// @ts-ignore
setUsers(filteredUsers || []);
}
} catch (error) {
console.error("Error fetching users:", error);
toast.error("Error loading users");
} finally {
setIsLoading(false);
}
};
const handleImpersonate = async () => {
if (!selectedUser) return;
try {
await authClient.admin.impersonateUser({
userId: selectedUser.id,
});
setIsImpersonating(true);
setOpen(false);
toast.success("Successfully impersonating user", {
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
});
window.location.reload();
} catch (error) {
console.error("Error impersonating user:", error);
toast.error("Error impersonating user");
}
};
const handleStopImpersonating = async () => {
try {
await authClient.admin.stopImpersonating();
setIsImpersonating(false);
setSelectedUser(null);
setShowBar(false);
toast.success("Stopped impersonating user");
window.location.reload();
} catch (error) {
console.error("Error stopping impersonation:", error);
toast.error("Error stopping impersonation");
}
};
useEffect(() => {
const checkImpersonation = async () => {
try {
const session = await authClient.getSession();
if (session?.data?.session?.impersonatedBy) {
setIsImpersonating(true);
setShowBar(true);
// setSelectedUser(data);
}
} catch (error) {
console.error("Error checking impersonation status:", error);
}
};
checkImpersonation();
fetchUsers();
}, []);
return (
<TooltipProvider>
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className={cn(
"fixed bottom-4 right-4 z-50 rounded-full shadow-lg",
isImpersonating &&
!showBar &&
"bg-red-100 hover:bg-red-200 border-red-200",
)}
onClick={() => setShowBar(!showBar)}
>
<Settings2
className={cn(
"h-4 w-4",
isImpersonating && !showBar && "text-red-500",
)}
/>
</Button>
</TooltipTrigger>
<TooltipContent>
{isImpersonating ? "Impersonation Controls" : "User Impersonation"}
</TooltipContent>
</Tooltip>
<div
className={cn(
"fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4 flex items-center justify-center gap-4 z-40 transition-all duration-200 ease-in-out",
showBar ? "translate-y-0" : "translate-y-full",
)}
>
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
<Logo className="w-10 h-10" />
{!isImpersonating ? (
<div className="flex items-center gap-2 w-full">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
aria-expanded={open}
className="w-[300px] justify-between"
>
{selectedUser ? (
<div className="flex items-center gap-2">
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="truncate flex flex-col items-start">
<span className="text-sm font-medium">
{selectedUser.name || ""}
</span>
<span className="text-xs text-muted-foreground">
{selectedUser.email}
</span>
</span>
</div>
) : (
<>
<UserIcon className="mr-2 h-4 w-4" />
<span>Select user to impersonate</span>
</>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput
placeholder="Search users by email or name..."
onValueChange={(search) => {
fetchUsers(search);
}}
className="h-9"
/>
{isLoading ? (
<div className="py-6 text-center text-sm">
Loading users...
</div>
) : (
<>
<CommandEmpty>No users found.</CommandEmpty>
<CommandList>
<CommandGroup heading="All Users">
{users.map((user) => (
<CommandItem
key={user.id}
value={user.email}
onSelect={() => {
setSelectedUser(user);
setOpen(false);
}}
>
<span className="flex items-center gap-2 flex-1">
<UserIcon className="h-4 w-4 flex-shrink-0" />
<span className="flex flex-col items-start">
<span className="text-sm font-medium">
{user.name || ""}
</span>
<span className="text-xs text-muted-foreground">
{user.email} {user.role}
</span>
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
selectedUser?.id === user.id
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</>
)}
</Command>
</PopoverContent>
</Popover>
<Button
onClick={handleImpersonate}
disabled={!selectedUser}
variant="default"
className="gap-2"
>
<Shield className="h-4 w-4" />
Impersonate
</Button>
</div>
) : (
<div className="flex items-center gap-4 w-full flex-wrap">
<div className="flex items-center gap-4 flex-1 flex-wrap">
<Avatar className="h-10 w-10">
<AvatarImage
src={data?.user?.image || ""}
alt={data?.user?.name || ""}
/>
<AvatarFallback>
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className="gap-1 py-1 text-yellow-500 bg-yellow-50/20"
>
<Shield className="h-3 w-3" />
Impersonating
</Badge>
<span className="font-medium">
{data?.user?.name || ""}
</span>
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
<span className="flex items-center gap-1">
<UserIcon className="h-3 w-3" />
{data?.user?.email} {data?.role}
</span>
<span className="flex items-center gap-1">
<Key className="h-3 w-3" />
<span className="flex items-center gap-1">
ID: {data?.user?.id?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
if (data?.id) {
copy(data.id);
toast.success("ID copied to clipboard");
}
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
<span className="flex items-center gap-1">
<Building2 className="h-3 w-3" />
<span className="flex items-center gap-1">
Org: {data?.organizationId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
if (data?.organizationId) {
copy(data.organizationId);
toast.success(
"Organization ID copied to clipboard",
);
}
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
{data?.user?.stripeCustomerId && (
<span className="flex items-center gap-1">
<CreditCard className="h-3 w-3" />
<span className="flex items-center gap-1">
Customer:
{data?.user?.stripeCustomerId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
copy(data?.user?.stripeCustomerId || "");
toast.success(
"Stripe Customer ID copied to clipboard",
);
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
)}
{data?.user?.stripeSubscriptionId && (
<span className="flex items-center gap-1">
<CreditCard className="h-3 w-3" />
<span className="flex items-center gap-1">
Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
copy(data.user.stripeSubscriptionId || "");
toast.success(
"Stripe Subscription ID copied to clipboard",
);
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
)}
{data?.user?.serversQuantity !== undefined && (
<span className="flex items-center gap-1">
<Server className="h-3 w-3" />
<span>Servers: {data.user.serversQuantity}</span>
</span>
)}
{data?.createdAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
Created:{" "}
{format(new Date(data.createdAt), "MMM d, yyyy")}
</span>
)}
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center gap-1 cursor-default">
<Fingerprint
className={cn(
"h-3 w-3",
data?.user?.twoFactorEnabled
? "text-green-500"
: "text-muted-foreground",
)}
/>
<Badge
variant={
data?.user?.twoFactorEnabled
? "green"
: "secondary"
}
className="text-[10px] px-1 py-0"
>
2FA{" "}
{data?.user?.twoFactorEnabled
? "Enabled"
: "Disabled"}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
Two-Factor Authentication Status
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<Button
onClick={handleStopImpersonating}
variant="secondary"
className="gap-2"
size="sm"
>
<XIcon className="w-4 h-4" />
Stop Impersonating
</Button>
</div>
)}
</div>
</div>
</>
</TooltipProvider>
);
};

View File

@@ -15,6 +15,7 @@ import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export type Services = {
appName: string;
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils();
const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
toast.success("Project duplicated successfully");
toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`);
if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
},
onError: (error) => {
toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
});
const handleDuplicate = async () => {
if (!name) {
if (duplicateType === "new-project" && !name) {
toast.error("Project name is required");
return;
}
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id,
type: service.type,
})),
duplicateInSameProject: duplicateType === "same-project",
});
};
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing
setName("");
setDescription("");
setDuplicateType("new-project");
}
}}
>
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle>
<DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription>
Create a new project with the selected services
Choose where to duplicate the selected services
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
<Label>Duplicate to</Label>
<RadioGroup
value={duplicateType}
onValueChange={setDuplicateType}
className="grid gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
{duplicateType === "new-project" && (
<>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating...
{duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
</>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : (
"Duplicate"
"Duplicate services"
)}
</Button>
</DialogFooter>

View File

@@ -10,6 +10,7 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -28,12 +29,14 @@ import { toast } from "sonner";
import { z } from "zod";
import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({
email: z.string(),
password: z.string().nullable(),
currentPassword: z.string().nullable(),
image: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false),
});
type Profile = z.infer<typeof profileSchema>;
@@ -56,6 +59,7 @@ const randomImages = [
export const ProfileForm = () => {
const _utils = api.useUtils();
const { data, refetch, isLoading } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const {
mutateAsync,
@@ -79,6 +83,7 @@ export const ProfileForm = () => {
password: "",
image: data?.user?.image || "",
currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false,
},
resolver: zodResolver(profileSchema),
});
@@ -91,11 +96,13 @@ export const ProfileForm = () => {
password: form.getValues("password") || "",
image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation,
},
{
keepValues: true,
},
);
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => {
@@ -111,6 +118,7 @@ export const ProfileForm = () => {
password: values.password || undefined,
image: values.image,
currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation,
})
.then(async () => {
await refetch();
@@ -256,7 +264,34 @@ export const ProfileForm = () => {
</FormItem>
)}
/>
{isCloud && (
<FormField
control={form.control}
name="allowImpersonation"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Allow Impersonation</FormLabel>
<FormDescription>
Enable this option to allow Dokploy Cloud
administrators to temporarily access your
account for troubleshooting and support
purposes. This helps them quickly identify and
resolve any issues you may encounter.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
<div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={isUpdating}>
{t("settings.common.save")}

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely.
</DialogDescription>
</DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && (
<AlertBlock type="warning">
You cannot create more servers,{" "}

View File

@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"

View File

@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
Cancel Invitation
</DropdownMenuItem>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success(
"Invitation removed",
);
});
}}
>
Remove Invitation
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success("Invitation removed");
});
}}
>
Remove Invitation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>

View File

@@ -1,9 +1,19 @@
import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api";
interface Props {
children: React.ReactNode;
metaName?: string;
}
export const DashboardLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
return (
<>
<Page>{children}</Page>
{haveRootAccess === true && <ImpersonationBar />}
</>
);
};

View File

@@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -631,6 +631,20 @@
"when": 1746392564463,
"tag": "0089_noisy_sandman",
"breakpoints": true
},
{
"idx": 90,
"version": "7",
"when": 1746509318678,
"tag": "0090_clean_wolf_cub",
"breakpoints": true
},
{
"idx": 91,
"version": "7",
"when": 1746518402168,
"tag": "0091_spotty_kulan_gath",
"breakpoints": true
}
]
}

View File

@@ -1,9 +1,15 @@
import { organizationClient } from "better-auth/client/plugins";
import { twoFactorClient } from "better-auth/client/plugins";
import { apiKeyClient } from "better-auth/client/plugins";
import { adminClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
// baseURL: "http://localhost:3000", // the base url of your auth server
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
plugins: [
organizationClient(),
twoFactorClient(),
apiKeyClient(),
adminClient(),
],
});

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.22.1",
"version": "v0.22.5",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -92,7 +92,7 @@
"adm-zip": "^0.5.14",
"ai": "^4.0.23",
"bcrypt": "5.1.1",
"better-auth": "1.2.6",
"better-auth": "v1.2.8-beta.7",
"bl": "6.0.11",
"boxen": "^7.1.1",
"bullmq": "5.4.2",

View File

@@ -80,7 +80,13 @@ export default function Custom404({ statusCode, error }: Props) {
<footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500">
Submit Log in issue on Github
<Link
href="https://github.com/Dokploy/dokploy/issues"
target="_blank"
className="underline hover:text-primary transition-colors"
>
Submit Log in issue on Github
</Link>
</p>
</div>
</footer>

View File

@@ -217,12 +217,12 @@ const Service = (
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "lg:grid-cols-9"
? "xl:grid-cols-9"
: data?.serverId
? "lg:grid-cols-8"
: "lg:grid-cols-9",
? "xl:grid-cols-8"
: "xl:grid-cols-9",
)}
>
<TabsTrigger value="general">General</TabsTrigger>

View File

@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
}
const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, {
const appName = `${projectName}-${generatePassword(6)}`;
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp,
projectName: projectName,
});
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
appName: appName,
isolatedDeployment: true,
});
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});

View File

@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
}),
)
.optional(),
duplicateInSameProject: z.boolean().default(false),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
});
}
// Create new project
const newProject = await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
? sourceProject
: await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({
...application,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
});
for (const domain of domains) {
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({
...postgres,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id);
const newMariadb = await createMariadb({
...mariadb,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id);
const newMongo = await createMongo({
...mongo,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id);
const newMysql = await createMysql({
...mysql,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({
...redis,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id);
const newCompose = await createCompose({
...compose,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (ctx.user.role === "member") {
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
newProject.projectId,
targetProject.projectId,
ctx.session.activeOrganizationId,
);
}
return newProject;
return targetProject;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",

View File

@@ -91,6 +91,18 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) {
return false;
}
if (
process.env.USER_ADMIN_ID === ctx.user.id ||
ctx.session?.impersonatedBy === process.env.USER_ADMIN_ID
) {
return true;
}
return false;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(

View File

@@ -31,7 +31,9 @@ import { ZodError } from "zod";
interface CreateContextOptions {
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
session: (Session & { activeOrganizationId: string }) | null;
session:
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
| null;
req: CreateNextContextOptions["req"];
res: CreateNextContextOptions["res"];
}

View File

@@ -0,0 +1,15 @@
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -40,7 +40,7 @@
"@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.6",
"better-auth": "v1.2.8-beta.7",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",

View File

@@ -57,8 +57,10 @@ export const users_temp = pgTable("user_temp", {
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
logCleanupCron: text("logCleanupCron"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
@@ -134,6 +136,8 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
const createSchema = createInsertSchema(users_temp, {
id: z.string().min(1),
isRegistered: z.boolean().optional(),
}).omit({
role: true,
});
export const apiCreateUserInvitation = createSchema.pick({}).extend({

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor } from "better-auth/plugins";
import { apiKey, organization, twoFactor, admin } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { db } from "../db";
@@ -187,9 +187,13 @@ const { handler, api } = betterAuth({
// required: true,
input: false,
},
allowImpersonation: {
fieldName: "allowImpersonation",
type: "boolean",
defaultValue: false,
},
},
},
plugins: [
apiKey({
enableMetadata: true,
@@ -214,6 +218,13 @@ const { handler, api } = betterAuth({
}
},
}),
...(IS_CLOUD
? [
admin({
adminUserIds: [process.env.USER_ADMIN_ID as string],
}),
]
: []),
],
});

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
dnf install -y wget git git-lfs jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl unzip >/dev/null
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
zypper install -y curl wget git git-lfs jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.35.0
export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
@@ -587,7 +587,7 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.0.37
export RAILPACK_VERSION=0.0.64
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi

View File

@@ -116,7 +116,7 @@ export const getComposeContainerCommand = (
composeType: "stack" | "docker-compose" | undefined,
) => {
if (composeType === "stack") {
return `docker ps -q --filter "status=running" --filter "label=com.docker.stack.namespace=${appName}" --filter "label=com.docker.swarm.service.name=${serviceName}" | head -n 1`;
return `docker ps -q --filter "status=running" --filter "label=com.docker.stack.namespace=${appName}" --filter "label=com.docker.swarm.service.name=${appName}_${serviceName}" | head -n 1`;
}
return `docker ps -q --filter "status=running" --filter "label=com.docker.compose.project=${appName}" --filter "label=com.docker.compose.service=${serviceName}" | head -n 1`;
};
@@ -159,7 +159,10 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
case "mysql": {
const mysql = backup.mysql;
if (backupType === "database" && mysql) {
return getMysqlBackupCommand(backup.database, mysql.databasePassword);
return getMysqlBackupCommand(
backup.database,
mysql.databaseRootPassword,
);
}
if (backupType === "compose" && backup.metadata?.mysql) {
return getMysqlBackupCommand(
@@ -231,7 +234,7 @@ export const getBackupCommand = (
fi
echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath};
# Run the backup command and capture the exit status
BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || {
echo "[$(date)] ❌ Error: Backup failed" >> ${logPath};
@@ -241,14 +244,14 @@ export const getBackupCommand = (
echo "[$(date)] ✅ backup completed successfully" >> ${logPath};
echo "[$(date)] Starting upload to S3..." >> ${logPath};
# Run the upload command and capture the exit status
UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || {
echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath};
echo "Error: $UPLOAD_OUTPUT" >> ${logPath};
exit 1;
}
echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath};
echo "Backup done ✅" >> ${logPath};
`;

View File

@@ -3,7 +3,7 @@ import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { mkdtemp } from "node:fs/promises";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
@@ -51,10 +51,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const postgresContainerId = containerId.trim();
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
// First dump the database inside the container
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
writeStream.write(`Running dump command: ${dumpCommand}\n`);
await execAsync(dumpCommand);
writeStream.write(`Running command: ${postgresCommand}\n`);
await execAsync(postgresCommand);
// Then copy the file from the container to host
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
writeStream.write(`Copying database dump: ${copyCommand}\n`);
await execAsync(copyCommand);
// Clean up the temp file in the container
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
await execAsync(cleanupCommand);
await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await updateDeploymentStatus(deployment.deploymentId, "done");
return true;
} finally {
await execAsync(`rm -rf ${tempDir}`);
try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
} catch (error) {
console.error("Backup error:", error);

View File

@@ -35,7 +35,7 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
if (compose.isolatedDeployment) {
await execAsync(
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}`,
`docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`,
);
}
@@ -190,7 +190,8 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || "";
let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}

View File

@@ -109,7 +109,7 @@ export const getDockerCommand = (
}
for (const arg of args) {
commandArgs.push("--build-arg", arg);
commandArgs.push("--build-arg", `'${arg}'`);
}
/*

View File

@@ -72,7 +72,7 @@ export const buildRailpack = async (
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
@@ -152,7 +152,7 @@ export const getRailpackCommand = (
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",

View File

@@ -246,32 +246,16 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
const allProjects = await validateGitlabProvider(gitlabProvider);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const filteredRepos = repositories.filter((repo: any) => {
const filteredRepos = allProjects.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) {
const isIncluded = groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
.some((name) => full_path === name);
return isIncluded && kind === "group";
}
@@ -432,34 +416,60 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const repositories = await response.json();
const repositories = await validateGitlabProvider(gitlabProvider);
const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace;
if (groupName) {
return groupName
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
return groupName.split(",").some((name) => full_path === name);
}
return kind === "user";
});
return filteredRepos.length;
};
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
try {
const allProjects = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Failed to fetch repositories: ${response.statusText}`,
});
}
const projects = await response.json();
if (projects.length === 0) {
break;
}
allProjects.push(...projects);
page++;
const total = response.headers.get("x-total");
if (total && allProjects.length >= Number.parseInt(total)) {
break;
}
}
return allProjects;
} catch (error) {
throw error;
}
};

View File

@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
database: string,
databaseUser: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} --clean --if-exists"`;
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
};
export const getMariadbRestoreCommand = (

66
pnpm-lock.yaml generated
View File

@@ -266,8 +266,8 @@ importers:
specifier: 5.1.1
version: 5.1.1(encoding@0.1.13)
better-auth:
specifier: 1.2.6
version: 1.2.6
specifier: v1.2.8-beta.7
version: 1.2.8-beta.7
bl:
specifier: 6.0.11
version: 6.0.11
@@ -300,10 +300,10 @@ importers:
version: 16.4.5
drizzle-orm:
specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
fancy-ansi:
specifier: ^0.1.3
version: 0.1.3
@@ -544,7 +544,7 @@ importers:
version: 16.4.5
drizzle-orm:
specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
hono:
specifier: ^4.5.8
version: 4.5.8
@@ -640,8 +640,8 @@ importers:
specifier: 5.1.1
version: 5.1.1(encoding@0.1.13)
better-auth:
specifier: 1.2.6
version: 1.2.6
specifier: v1.2.8-beta.7
version: 1.2.8-beta.7
bl:
specifier: 6.0.11
version: 6.0.11
@@ -659,13 +659,13 @@ importers:
version: 16.4.5
drizzle-dbml-generator:
specifier: 0.10.0
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
drizzle-orm:
specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
hi-base32:
specifier: ^0.5.1
version: 0.5.1
@@ -927,6 +927,9 @@ packages:
'@better-auth/utils@0.2.4':
resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==}
'@better-auth/utils@0.2.5':
resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==}
'@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
@@ -3836,11 +3839,11 @@ packages:
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
better-auth@1.2.6:
resolution: {integrity: sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ==}
better-auth@1.2.8-beta.7:
resolution: {integrity: sha512-gVApvvhnPVqMCYYLMhxUfbTi5fJYfp9rcsoJSjjTOMV+CIc7KVlYN6Qo8E7ju1JeRU5ae1Wl1NdXrolRJHjmaQ==}
better-call@1.0.7:
resolution: {integrity: sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg==}
better-call@1.0.9:
resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@@ -5289,9 +5292,9 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kysely@0.27.6:
resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
engines: {node: '>=14.0.0'}
kysely@0.28.2:
resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==}
engines: {node: '>=18.0.0'}
leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
@@ -7564,6 +7567,11 @@ snapshots:
typescript: 5.8.3
uncrypto: 0.1.3
'@better-auth/utils@0.2.5':
dependencies:
typescript: 5.8.3
uncrypto: 0.1.3
'@better-fetch/fetch@1.1.18': {}
'@biomejs/biome@1.9.4':
@@ -10540,22 +10548,22 @@ snapshots:
before-after-hook@2.2.3: {}
better-auth@1.2.6:
better-auth@1.2.8-beta.7:
dependencies:
'@better-auth/utils': 0.2.4
'@better-auth/utils': 0.2.5
'@better-fetch/fetch': 1.1.18
'@noble/ciphers': 0.6.0
'@noble/hashes': 1.7.1
'@simplewebauthn/browser': 13.1.0
'@simplewebauthn/server': 13.1.1
better-call: 1.0.7
better-call: 1.0.9
defu: 6.1.4
jose: 5.9.6
kysely: 0.27.6
kysely: 0.28.2
nanostores: 0.11.3
zod: 3.24.1
better-call@1.0.7:
better-call@1.0.9:
dependencies:
'@better-fetch/fetch': 1.1.18
rou3: 0.5.1
@@ -11169,9 +11177,9 @@ snapshots:
drange@1.1.1: {}
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
dependencies:
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-kit@0.30.4:
dependencies:
@@ -11182,18 +11190,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/react': 18.3.5
kysely: 0.27.6
kysely: 0.28.2
postgres: 3.4.4
react: 18.2.0
sqlite3: 5.1.7
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
dependencies:
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.28.2)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
zod: 3.23.8
eastasianwidth@0.2.0: {}
@@ -12082,7 +12090,7 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kysely@0.27.6: {}
kysely@0.28.2: {}
leac@0.6.0: {}