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 # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | 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 \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.0.37 ARG RAILPACK_VERSION=0.0.64
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 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> </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 🥈 --> <!-- Elite Contributors 🥈 -->
<!-- Add Elite Contributors here --> <!-- Add Elite Contributors here -->
### Supporting Members 🥉 ### 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://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://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> <a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div> </div>

View File

@@ -8,7 +8,7 @@ COPY . /usr/src/app
WORKDIR /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 # Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 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 = { const baseAdmin: User = {
https: false, https: false,
enablePaidFeatures: false, enablePaidFeatures: false,
allowImpersonation: false,
role: "user",
metricsConfig: { metricsConfig: {
containers: { containers: {
refreshRate: 20, refreshRate: 20,

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => ( {templateInfo.template.envs.map((env, index) => (
<div <div
key={index} 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} {env}
</div> </div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription> <DialogDescription>Mount File Content</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="h-[25vh] pr-4"> <ScrollArea className="h-[45vh] pr-4">
<CodeEditor <CodeEditor
language="yaml" language="yaml"
value={selectedMount?.content || ""} value={selectedMount?.content || ""}

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -200,6 +201,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Form {...form}> <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 <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 p-2" className="grid w-full gap-4 p-2"

View File

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

View File

@@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => {
return ( return (
<Card <Card
key={item.domainId} 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"> <CardContent className="p-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Service & Domain Info */} {/* Service & Domain Info */}
<div className="flex items-start justify-between"> <div className="flex items-center justify-between flex-wrap gap-y-2">
<div className="flex flex-col gap-2"> {item.serviceName && (
{item.serviceName && ( <Badge variant="outline" className="w-fit">
<Badge variant="outline" className="w-fit"> <Server className="size-3 mr-1" />
<Server className="size-3 mr-1" /> {item.serviceName}
{item.serviceName} </Badge>
</Badge> )}
)} <div className="flex gap-2 flex-wrap">
<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">
{!item.host.includes("traefik.me") && ( {!item.host.includes("traefik.me") && (
<DnsHelperModal <DnsHelperModal
domain={{ domain={{
@@ -266,6 +255,16 @@ export const ShowDomains = ({ id, type }: Props) => {
</DialogAction> </DialogAction>
</div> </div>
</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 */} {/* Domain Details */}
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react"; import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect. one domain must be specified for this conversion to take effect.
</AlertBlock> </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"> <pre>
<Button <CodeEditor
variant="secondary" value={compose || ""}
isLoading={isLoading} language="yaml"
onClick={() => { readOnly
mutateAsync({ composeId }) height="50rem"
.then(() => { />
refetch(); </pre>
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>
</DialogContent> </DialogContent>
</Dialog> </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 { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
await utils.project.all.invalidate(); await utils.project.all.invalidate();
toast.success("Project duplicated successfully"); toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false); setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`); if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
}, },
onError: (error) => { onError: (error) => {
toast.error(error.message); toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
}); });
const handleDuplicate = async () => { const handleDuplicate = async () => {
if (!name) { if (duplicateType === "new-project" && !name) {
toast.error("Project name is required"); toast.error("Project name is required");
return; return;
} }
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id, id: service.id,
type: service.type, type: service.type,
})), })),
duplicateInSameProject: duplicateType === "same-project",
}); });
}; };
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing // Reset form when closing
setName(""); setName("");
setDescription(""); setDescription("");
setDuplicateType("new-project");
} }
}} }}
> >
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle> <DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription> <DialogDescription>
Create a new project with the selected services Choose where to duplicate the selected services
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="name">Name</Label> <Label>Duplicate to</Label>
<Input <RadioGroup
id="name" value={duplicateType}
value={name} onValueChange={setDuplicateType}
onChange={(e) => setName(e.target.value)} className="grid gap-2"
placeholder="New project name" >
/> <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>
<div className="grid gap-2"> {duplicateType === "new-project" && (
<Label htmlFor="description">Description</Label> <>
<Input <div className="grid gap-2">
id="description" <Label htmlFor="name">Name</Label>
value={description} <Input
onChange={(e) => setDescription(e.target.value)} id="name"
placeholder="Project description (optional)" value={name}
/> onChange={(e) => setName(e.target.value)}
</div> 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"> <div className="grid gap-2">
<Label>Selected services to duplicate</Label> <Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <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> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -10,6 +10,7 @@ import {
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@@ -28,12 +29,14 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Disable2FA } from "./disable-2fa"; import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa"; import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({ const profileSchema = z.object({
email: z.string(), email: z.string(),
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false),
}); });
type Profile = z.infer<typeof profileSchema>; type Profile = z.infer<typeof profileSchema>;
@@ -56,6 +59,7 @@ const randomImages = [
export const ProfileForm = () => { export const ProfileForm = () => {
const _utils = api.useUtils(); const _utils = api.useUtils();
const { data, refetch, isLoading } = api.user.get.useQuery(); const { data, refetch, isLoading } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { const {
mutateAsync, mutateAsync,
@@ -79,6 +83,7 @@ export const ProfileForm = () => {
password: "", password: "",
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false,
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
}); });
@@ -91,11 +96,13 @@ export const ProfileForm = () => {
password: form.getValues("password") || "", password: form.getValues("password") || "",
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation,
}, },
{ {
keepValues: true, keepValues: true,
}, },
); );
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
if (data.user.email) { if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => { generateSHA256Hash(data.user.email).then((hash) => {
@@ -111,6 +118,7 @@ export const ProfileForm = () => {
password: values.password || undefined, password: values.password || undefined,
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@@ -256,7 +264,34 @@ export const ProfileForm = () => {
</FormItem> </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>
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={isUpdating}> <Button type="submit" isLoading={isUpdating}>
{t("settings.common.save")} {t("settings.common.save")}

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely. remotely.
</DialogDescription> </DialogDescription>
</DialogHeader> </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 && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}

View File

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

View File

@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
Cancel Invitation Cancel Invitation
</DropdownMenuItem> </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> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</TableCell> </TableCell>

View File

@@ -1,9 +1,19 @@
import Page from "./side"; import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
metaName?: string; metaName?: string;
} }
export const DashboardLayout = ({ children }: Props) => { 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, "when": 1746392564463,
"tag": "0089_noisy_sandman", "tag": "0089_noisy_sandman",
"breakpoints": true "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 { organizationClient } from "better-auth/client/plugins";
import { twoFactorClient } from "better-auth/client/plugins"; import { twoFactorClient } from "better-auth/client/plugins";
import { apiKeyClient } 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"; import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
// baseURL: "http://localhost:3000", // the base url of your auth server // 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", "name": "dokploy",
"version": "v0.22.1", "version": "v0.22.5",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -92,7 +92,7 @@
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"ai": "^4.0.23", "ai": "^4.0.23",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"better-auth": "1.2.6", "better-auth": "v1.2.8-beta.7",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.4.2", "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"> <footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500"> <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> </p>
</div> </div>
</footer> </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"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( 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 isCloud && data?.serverId
? "lg:grid-cols-9" ? "xl:grid-cols-9"
: data?.serverId : data?.serverId
? "lg:grid-cols-8" ? "xl:grid-cols-8"
: "lg:grid-cols-9", : "xl:grid-cols-9",
)} )}
> >
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>

View File

@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
} }
const projectName = slugify(`${project.name} ${input.id}`); 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, serverIp: serverIp,
projectName: projectName, projectName: projectName,
}); });
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId, serverId: input.serverId,
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: appName,
isolatedDeployment: true, 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, serverIp: serverIp,
projectName: compose.appName, 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, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });

View File

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

View File

@@ -91,6 +91,18 @@ export const userRouter = createTRPCRouter({
return memberResult; 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 }) => { getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({ const memberResult = await db.query.member.findFirst({
where: and( where: and(

View File

@@ -31,7 +31,9 @@ import { ZodError } from "zod";
interface CreateContextOptions { interface CreateContextOptions {
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null; user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
session: (Session & { activeOrganizationId: string }) | null; session:
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
| null;
req: CreateNextContextOptions["req"]; req: CreateNextContextOptions["req"];
res: CreateNextContextOptions["res"]; 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/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0", "drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.6", "better-auth": "v1.2.8-beta.7",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4", "@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21", "@react-email/components": "^0.0.21",

View File

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

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api"; 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 { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { db } from "../db"; import { db } from "../db";
@@ -187,9 +187,13 @@ const { handler, api } = betterAuth({
// required: true, // required: true,
input: false, input: false,
}, },
allowImpersonation: {
fieldName: "allowImpersonation",
type: "boolean",
defaultValue: false,
},
}, },
}, },
plugins: [ plugins: [
apiKey({ apiKey({
enableMetadata: true, 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 case "$OS_TYPE" in
arch) 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) alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null 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) ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 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) centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then 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 else
if ! command -v dnf >/dev/null; then if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null dnf install -y curl >/dev/null
fi fi
dnf install -y wget git jq openssl unzip >/dev/null dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi fi
;; ;;
sles | opensuse-leap | opensuse-tumbleweed) sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 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." 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 if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.35.0 export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi
@@ -587,7 +587,7 @@ const installRailpack = () => `
if command_exists railpack; then if command_exists railpack; then
echo "Railpack already installed ✅" echo "Railpack already installed ✅"
else else
export RAILPACK_VERSION=0.0.37 export RAILPACK_VERSION=0.0.64
bash -c "$(curl -fsSL https://railpack.com/install.sh)" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅" echo "Railpack version $RAILPACK_VERSION installed ✅"
fi fi

View File

@@ -116,7 +116,7 @@ export const getComposeContainerCommand = (
composeType: "stack" | "docker-compose" | undefined, composeType: "stack" | "docker-compose" | undefined,
) => { ) => {
if (composeType === "stack") { 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`; 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": { case "mysql": {
const mysql = backup.mysql; const mysql = backup.mysql;
if (backupType === "database" && mysql) { if (backupType === "database" && mysql) {
return getMysqlBackupCommand(backup.database, mysql.databasePassword); return getMysqlBackupCommand(
backup.database,
mysql.databaseRootPassword,
);
} }
if (backupType === "compose" && backup.metadata?.mysql) { if (backupType === "compose" && backup.metadata?.mysql) {
return getMysqlBackupCommand( return getMysqlBackupCommand(
@@ -231,7 +234,7 @@ export const getBackupCommand = (
fi fi
echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath}; echo "[$(date)] Container Up: $CONTAINER_ID" >> ${logPath};
# Run the backup command and capture the exit status # Run the backup command and capture the exit status
BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || { BACKUP_OUTPUT=$(${backupCommand} 2>&1 >/dev/null) || {
echo "[$(date)] ❌ Error: Backup failed" >> ${logPath}; echo "[$(date)] ❌ Error: Backup failed" >> ${logPath};
@@ -241,14 +244,14 @@ export const getBackupCommand = (
echo "[$(date)] ✅ backup completed successfully" >> ${logPath}; echo "[$(date)] ✅ backup completed successfully" >> ${logPath};
echo "[$(date)] Starting upload to S3..." >> ${logPath}; echo "[$(date)] Starting upload to S3..." >> ${logPath};
# Run the upload command and capture the exit status # Run the upload command and capture the exit status
UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || { UPLOAD_OUTPUT=$(${backupCommand} | ${rcloneCommand} 2>&1 >/dev/null) || {
echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath}; echo "[$(date)] ❌ Error: Upload to S3 failed" >> ${logPath};
echo "Error: $UPLOAD_OUTPUT" >> ${logPath}; echo "Error: $UPLOAD_OUTPUT" >> ${logPath};
exit 1; exit 1;
} }
echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath}; echo "[$(date)] ✅ Upload to S3 completed successfully" >> ${logPath};
echo "Backup done ✅" >> ${logPath}; echo "Backup done ✅" >> ${logPath};
`; `;

View File

@@ -3,7 +3,7 @@ import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils"; import { getS3Credentials, normalizeS3Path } from "./utils";
import { findDestinationById } from "@dokploy/server/services/destination"; import { findDestinationById } from "@dokploy/server/services/destination";
import { IS_CLOUD, paths } from "@dokploy/server/constants"; 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 { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { import {
@@ -51,10 +51,20 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
const postgresContainerId = containerId.trim(); 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`); // Then copy the file from the container to host
await execAsync(postgresCommand); 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( await execAsync(
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`, `rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
return true; return true;
} finally { } finally {
await execAsync(`rm -rf ${tempDir}`); try {
await rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
} }
} catch (error) { } catch (error) {
console.error("Backup error:", error); console.error("Backup error:", error);

View File

@@ -35,7 +35,7 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
if (compose.isolatedDeployment) { if (compose.isolatedDeployment) {
await execAsync( 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"); join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }
@@ -219,7 +220,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFilePath = join(dirname(composeFilePath), ".env"); const envFilePath = join(dirname(composeFilePath), ".env");
let envContent = env || ""; let envContent = `APP_NAME=${appName}\n`;
envContent += env || "";
if (!envContent.includes("DOCKER_CONFIG")) { if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
} }

View File

@@ -109,7 +109,7 @@ export const getDockerCommand = (
} }
for (const arg of args) { 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", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55", "BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
"-f", "-f",
`${buildAppDirectory}/railpack-plan.json`, `${buildAppDirectory}/railpack-plan.json`,
"--output", "--output",
@@ -152,7 +152,7 @@ export const getRailpackCommand = (
] ]
: []), : []),
"--build-arg", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55", "BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
"-f", "-f",
`${buildAppDirectory}/railpack-plan.json`, `${buildAppDirectory}/railpack-plan.json`,
"--output", "--output",

View File

@@ -246,32 +246,16 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const allProjects = await validateGitlabProvider(gitlabProvider);
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!response.ok) { const filteredRepos = allProjects.filter((repo: any) => {
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 { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
const groupName = gitlabProvider.groupName?.toLowerCase(); const groupName = gitlabProvider.groupName?.toLowerCase();
if (groupName) { if (groupName) {
const isIncluded = groupName const isIncluded = groupName
.split(",") .split(",")
.some((name) => full_path.toLowerCase().includes(name)); .some((name) => full_path === name);
return isIncluded && kind === "group"; return isIncluded && kind === "group";
} }
@@ -432,34 +416,60 @@ export const testGitlabConnection = async (
const gitlabProvider = await findGitlabById(gitlabId); const gitlabProvider = await findGitlabById(gitlabId);
const response = await fetch( const repositories = await validateGitlabProvider(gitlabProvider);
`${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 filteredRepos = repositories.filter((repo: any) => { const filteredRepos = repositories.filter((repo: any) => {
const { full_path, kind } = repo.namespace; const { full_path, kind } = repo.namespace;
if (groupName) { if (groupName) {
return groupName return groupName.split(",").some((name) => full_path === name);
.split(",")
.some((name) => full_path.toLowerCase().includes(name));
} }
return kind === "user"; return kind === "user";
}); });
return filteredRepos.length; 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, database: string,
databaseUser: 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 = ( export const getMariadbRestoreCommand = (

66
pnpm-lock.yaml generated
View File

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