Compare commits

..

74 Commits

Author SHA1 Message Date
Mauricio Siu
9d0f5bc8cd Update package.json 2025-04-02 00:31:55 -06:00
Mauricio Siu
3dc558c260 Merge pull request #1599 from vicke4/backup-cron-fix-attempt
fix(backups): awaiting initCronJobs call in an attempt to fix backups cron
2025-04-02 00:31:20 -06:00
vicke4
180aa34140 fix(backups): awaiting initcronjobs in an attempt to fix backups cron 2025-04-02 11:07:48 +05:30
Mauricio Siu
ffc85b04a8 Merge pull request #1572 from thebadking/refactor-show-build-form-and-prettier
build form optimization
2025-03-30 03:36:19 -06:00
Mauricio Siu
dbcfc702d4 Merge branch 'canary' into refactor-show-build-form-and-prettier 2025-03-30 03:31:49 -06:00
Mauricio Siu
7805efc738 Merge pull request #1584 from Dokploy/1294-duplicate-project-andor-services-accross-projects
1294 duplicate project andor services accross projects
2025-03-30 02:50:09 -06:00
Mauricio Siu
3910e22412 Refactor Duplicate Project component and integrate with project detail page
- Updated the DuplicateProject component to accept projectId and selectedServiceIds as props, enhancing its flexibility.
- Removed unnecessary state management for service selection within the component.
- Integrated the DuplicateProject component directly into the project detail page, allowing for easier access to duplication functionality.
- Improved the user interface by utilizing DialogTrigger for initiating the duplication process and displaying selected services more clearly.
2025-03-30 02:42:35 -06:00
Mauricio Siu
2f16034cb0 Add Duplicate Project functionality
- Introduced a new component for duplicating projects, allowing users to create a new project with the same configuration as an existing one.
- Implemented a mutation in the project router to handle project duplication, including optional service duplication.
- Updated the project detail page to include a dropdown menu for initiating the duplication process.
- Enhanced the API to validate and process the duplication request, ensuring proper handling of services associated with the project.
2025-03-30 02:38:53 -06:00
Mauricio Siu
d4925dd2b7 Update dokploy version to v0.21.0 in package.json 2025-03-30 01:59:43 -06:00
Mauricio Siu
5aba6c79a0 Merge pull request #1582 from Dokploy/fix/allow-false-values-in-env
Fix/allow false values in env
2025-03-30 01:40:06 -06:00
Mauricio Siu
84f5627471 Enhance environment variable handling in processTemplate: support boolean and number types in env configuration, with tests for both array and object formats. 2025-03-30 01:29:23 -06:00
Mauricio Siu
4eaf8fee0f Add toml package and update configuration parsing in Dokploy
- Added toml package version 3.0.0 to package.json files in both apps/dokploy and packages/server.
- Replaced js-yaml load function with toml parse function for configuration parsing in compose.ts and github.ts files, ensuring compatibility with TOML format.
- Updated pnpm-lock.yaml to reflect the new toml dependency.
2025-03-30 01:12:27 -06:00
Mauricio Siu
adee87b6da Enhance volume handling in Docker Compose: update addSuffixToVolumesInServices to correctly manage volume paths with subdirectories and improve test coverage for suffix changes in volume names. 2025-03-30 00:32:09 -06:00
Mauricio Siu
e5e987fcf9 Merge branch 'canary' into fix/allow-false-values-in-env 2025-03-29 23:51:48 -06:00
Mauricio Siu
d0a6373dcc Update Dockerfile to include 'zip' package in production environment for enhanced file handling capabilities. 2025-03-29 23:49:45 -06:00
Mauricio Siu
8ed44066ad Update Card component in server settings page: adjust width class for better responsiveness and layout consistency. 2025-03-29 23:38:28 -06:00
Mauricio Siu
befe2193a7 Merge pull request #1581 from Dokploy/709-back-ups-for-dokploy
709 back ups for dokploy
2025-03-29 23:27:29 -06:00
Mauricio Siu
f20c73cdee Refactor temporary directory creation in web server backup: replace static path with dynamic temp directory using mkdtemp for improved file management and isolation during backup operations. 2025-03-29 23:27:13 -06:00
Mauricio Siu
16bfc09202 Refactor serverId assignment in ShowBackups component: update logic to check for serverId presence in postgres object, improving code clarity and consistency. 2025-03-29 20:24:39 -06:00
Mauricio Siu
d54a61b2a4 Remove commented-out backup commands from restoreWebServerBackup function for cleaner code and improved readability. 2025-03-29 20:14:55 -06:00
Mauricio Siu
60c09a6434 Implement cloud check in backup and restore functions: add IS_CLOUD condition to skip operations for cloud environments in runWebServerBackup and restoreWebServerBackup functions. 2025-03-29 20:14:36 -06:00
Mauricio Siu
5361e9074f Refactor backup scheduling: consolidate backup handling for multiple database types into a single loop, enhance error logging, and integrate web server backup functionality. 2025-03-29 20:12:55 -06:00
Mauricio Siu
13d4dea504 Enhance ShowBackups component: add Database icon to title for improved UI clarity and wrap ShowBackups in a Card component for better layout in server settings page. 2025-03-29 19:53:25 -06:00
Mauricio Siu
ffc2d593e4 Refactor restoreWebServerBackup function: implement temporary directory creation outside BASE_PATH, streamline filesystem restoration process, and enhance logging for database restore operations. 2025-03-29 19:47:02 -06:00
Mauricio Siu
297439a348 Update restore-backup component and backup router for web server support: set default database name based on type, disable input for web server, and streamline backup restoration process with improved logging and error handling. 2025-03-29 19:27:05 -06:00
Mauricio Siu
ff3e067866 Implement web server backup and restore functionality: add new backup and restore methods for web servers, including S3 integration and improved logging. Refactor existing backup process to support web server type and streamline temporary file management. 2025-03-29 18:43:35 -06:00
Mauricio Siu
f008a45bf2 Enhance backup process: implement temporary directory structure for backups, add filesystem backup, and create zip file of database and filesystem contents. Update logging for better visibility during backup operations. 2025-03-29 18:00:13 -06:00
Mauricio Siu
50c8503cf9 Enhance backup functionality: add support for 'web-server' database type in backup components, update related schemas, and implement new backup procedures. Introduce userId reference in backups schema and adjust UI components to accommodate the new type. 2025-03-29 17:53:57 -06:00
Mauricio Siu
930a03de60 Merge pull request #1523 from jrparks/feat/add-gitea-repo
Feat/add gitea repo
2025-03-29 15:43:55 -06:00
autofix-ci[bot]
2d3d86e823 [autofix.ci] apply automated fixes 2025-03-29 21:18:08 +00:00
André Ferreira
7bab166e1b increased type safety 2025-03-29 21:17:32 +00:00
Mauricio Siu
7a6e1dbc1b Refactor SecurityAudit component: remove unused root login security check and related UI elements to streamline the code and improve readability. 2025-03-29 15:04:33 -06:00
Mauricio Siu
17a859d26d Refactor ShowConvertedCompose component: simplify conditional rendering of the compose file display by removing unnecessary null check and ensuring consistent layout. 2025-03-29 15:01:55 -06:00
Mauricio Siu
d793c6a2ec Add Gitea repository link in SaveGiteaProvider: integrate dynamic repository URL display and enhance user experience with a view link for existing repositories. 2025-03-29 15:00:23 -06:00
Mauricio Siu
3adb9d54f4 Refactor ShowProviderFormCompose component: streamline provider rendering logic and ensure Gitea integration is fully functional with updated tab and content handling. 2025-03-29 14:57:36 -06:00
Mauricio Siu
7144adbf0c Add migration for '0081_lovely_mentallo': remove gitea_username column and update journal with new version 2025-03-29 14:47:51 -06:00
Mauricio Siu
55328468d1 Refactor Gitea integration: remove giteaProjectId references and update related schemas. Add new fields for gitea repository details in application tests and components. 2025-03-29 14:44:33 -06:00
Mauricio Siu
fe967239b4 Merge branch 'canary' into feat/add-gitea-repo 2025-03-29 13:47:18 -06:00
André Ferreira
1f28a21835 remove prettier 2025-03-28 19:21:39 +00:00
André Ferreira
0114b371f5 prettier and build form optimization 2025-03-28 17:31:53 +00:00
Jason Parks
66d6cb5710 Fixed compose bug and formatted. Updated the refresh token to check the expired time. 2025-03-27 15:27:53 -06:00
Jason Parks
5927c7c3c5 Added back in compose alert block 2025-03-27 13:26:24 -06:00
Jason Parks
84afcf0de5 Removed apps/dokploy/templates/infisical/index.ts 2025-03-27 13:12:02 -06:00
Mauricio Siu
e3527f7d69 fix(processors): ensure environment variable processing handles non-string values correctly 2025-03-26 01:48:50 -06:00
Jason Parks
39f4a35cc8 fix: remove giteaPathNamespace 2025-03-24 01:53:00 -06:00
Jason Parks
5d5913f39d fix: resolve aria-hidden accessibility error in dialog component
Remove custom onOpenAutoFocus handler that was preventing proper focus management in the EditGiteaProvider dialog. This fixes the 'Blocked aria-hidden on an element because its descendant retained focus' error by allowing the dialog component to handle focus naturally.
2025-03-23 16:13:16 -06:00
Jason Parks
f04c8a36af Fix Gitea repository integration and preview compose display 2025-03-23 15:35:22 -06:00
Jason Parks
d5137d5d3a fix: handle repository id null case consistently 2025-03-23 14:04:30 -06:00
Mauricio Siu
048c8ffc11 feat: Add Docker image push notification in commit message extraction
- Enhanced the extractCommitMessage function to include a specific message format for Docker image pushes when the user-agent indicates a Go-http-client. This improves clarity in deployment notifications by providing detailed information about the repository and the pusher.
2025-03-23 04:12:08 -06:00
Mauricio Siu
b59597630c refactor: Remove commented-out Gitea requirements check in Git providers component
- Eliminated unnecessary commented code related to Gitea access token checks in the ShowGitProviders component to enhance code clarity and maintainability.
2025-03-23 04:10:00 -06:00
Mauricio Siu
707463f973 refactor: Streamline Docker service management and error handling
- Removed unnecessary console logging in the mechanizeDockerContainer function to enhance code clarity.
- Simplified error handling by directly creating a new service if the existing one is not found, improving the deployment logic.
- Updated the buildNixpacks function to ensure container cleanup is attempted without additional error handling, streamlining the process.
2025-03-23 04:09:06 -06:00
Mauricio Siu
4b3e0805a4 chore: Update dependencies and clean up code formatting
- Downgraded '@trpc/server' version in package.json and pnpm-lock.yaml to match other TRPC packages.
- Removed unused dependencies 'node-fetch', 'data-uri-to-buffer', 'fetch-blob', and 'formdata-polyfill' from pnpm-lock.yaml and package.json.
- Improved code formatting in various components for better readability, including adjustments in 'edit-gitea-provider.tsx' and 'security-audit.tsx'.
- Refactored imports in 'auth.ts' for better organization and clarity.
2025-03-23 04:06:35 -06:00
Mauricio Siu
148c30f604 refactor: Remove unnecessary logging in Gitea repository fetching
- Eliminated console logging of repositories in the Gitea API router to streamline the code and improve performance.
2025-03-23 03:57:37 -06:00
Mauricio Siu
95f79f2afb feat: Enhance deployment logic for multiple Git providers
- Added support for handling commit normalization across GitHub, GitLab, and Gitea in the deployment API.
- Implemented a new utility function to determine the provider based on request headers.
- Improved deployment path validation to ensure consistency across different source types.
- Cleaned up the code by removing redundant checks and enhancing readability.
2025-03-23 03:55:11 -06:00
Mauricio Siu
a067abd3e4 refactor: Enhance Gitea repository handling and clean up token refresh logic
- Updated Gitea repository cloning to use the remote variant for better consistency.
- Removed unnecessary logging and comments in the token refresh function to streamline the code.
- Improved type handling in the cloneGiteaRepository function for better clarity.
2025-03-23 03:35:02 -06:00
Mauricio Siu
9359ee7a04 refactor: Update Gitea provider components and API handling
- Adjusted GiteaProviderSchema to ensure watchPaths are correctly initialized and validated.
- Refactored SaveGiteaProvider and SaveGiteaProviderCompose components for improved state management and UI consistency.
- Simplified API router methods for Gitea, enhancing readability and error handling.
- Updated database schema and service functions for better clarity and maintainability.
- Removed unnecessary comments and improved logging for better debugging.
2025-03-23 03:27:19 -06:00
Jason Parks
fc7eff94b6 fix: Security Audit SSH Errors #1377
- Fixed SSH key authentication detection in server-audit.ts
- Added proper handling for prohibit-password and other secure root login options
- Fixed typos in security audit UI labels
- Improved error handling with optional chaining
2025-03-22 14:26:40 -06:00
Jason Parks
ff3d444b89 fix: prevent form dropdown flicker in Gitea provider modal
Prevents the brief appearance of dropdown options when opening the Edit Gitea Provider modal by:
- Adding onOpenAutoFocus event handler to prevent automatic focus
- Setting autoFocus={false} on the first input field
- Simplifying component state management

This improves the UI experience by eliminating visual artifacts when the dialog opens.
2025-03-22 13:30:47 -06:00
Jason Parks
530ad31aaa Simplify Gitea authorization flow with shared utilities 2025-03-20 16:48:59 -06:00
Jason Parks
a4e4d1c467 Fix Gitea watch branch validation logic 2025-03-20 13:50:55 -06:00
Jason Parks
56d8defebe Added watchlist paths for Gitea and some minor typescript fixes. 2025-03-19 16:48:51 -06:00
Jason Parks
997e755b6f Merge remote-tracking branch 'upstream/canary' into feat/add-gitea-repo 2025-03-19 14:06:29 -06:00
Jason Parks
852011dde8 Merge branch 'feat/add-gitea-repo' of https://github.com/jrparks/dokploy into feat/add-gitea-repo 2025-03-19 11:32:29 -06:00
Jason Parks
d7ef201adb Refactor Gitea integration and update related components 2025-03-19 11:31:08 -06:00
Mauricio Siu
4eef65f1b7 fix(git): update Gitea provider instructions and improve Git providers display
- Enhanced the Gitea provider setup instructions by specifying the navigation path for creating a new OAuth2 application.
- Added a blank line for better readability in the Git providers display component.
2025-03-18 21:50:01 -06:00
Mauricio Siu
a7535c6862 Merge branch 'canary' into feat/add-gitea-repo 2025-03-18 21:42:42 -06:00
Mauricio Siu
ff22404b3b feat(gitea): add Gitea integration with database schema updates
- Introduced Gitea support by adding necessary database tables and columns.
- Updated enum types to include 'gitea' for source and git provider types.
- Established foreign key relationships between Gitea and application/compose tables.
- Removed obsolete Gitea-related SQL files and updated journal entries for clarity.
2025-03-18 01:16:38 -06:00
Mauricio Siu
17330ca71a refactor: standardize import statements and improve code structure across multiple components
- Updated import statements to maintain consistency and clarity.
- Refactored components to enhance readability and organization.
- Ensured proper usage of type imports and removed unnecessary comments.
- Improved user feedback mechanisms in forms and alerts for better user experience.
2025-03-18 00:52:34 -06:00
Mauricio Siu
2898a5e575 Merge branch 'canary' into feat/add-gitea-repo 2025-03-18 00:50:27 -06:00
Jason Parks
fac8ea7a30 Merge branch 'canary' into feat/add-gitea-repo 2025-03-17 15:25:02 -06:00
Jason Parks
9a11d0db97 feat(gitea): add Gitea repository support 2025-03-17 15:17:35 -06:00
Jason Parks
cf28640188 Merge branch 'Dokploy:canary' into canary 2025-03-16 13:13:41 -06:00
Jason Parks
ea39b152f4 fix: resolved merge conflicts with fork/canary 2025-03-16 03:02:15 -06:00
Jason Parks
027406547e feat(gitea): Added Gitea Repo Integration 2025-03-16 02:11:48 -06:00
127 changed files with 31994 additions and 671 deletions

View File

@@ -29,7 +29,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next

View File

@@ -1006,7 +1006,7 @@ services:
volumes:
db-config-testhash:
`) as ComposeSpecification;
`);
test("Expect to change the suffix in all the possible places (4 Try)", () => {
const composeData = load(composeFileComplex) as ComposeSpecification;
@@ -1115,3 +1115,60 @@ test("Expect to change the suffix in all the possible places (5 Try)", () => {
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
});
const composeFileBackrest = `
services:
backrest:
image: garethgeorge/backrest:v1.7.3
restart: unless-stopped
ports:
- 9898
environment:
- BACKREST_PORT=9898
- BACKREST_DATA=/data
- BACKREST_CONFIG=/config/config.json
- XDG_CACHE_HOME=/cache
- TZ=\${TZ}
volumes:
- backrest/data:/data
- backrest/config:/config
- backrest/cache:/cache
- /:/userdata:ro
volumes:
backrest:
backrest-cache:
`;
const expectedDockerComposeBackrest = load(`
services:
backrest:
image: garethgeorge/backrest:v1.7.3
restart: unless-stopped
ports:
- 9898
environment:
- BACKREST_PORT=9898
- BACKREST_DATA=/data
- BACKREST_CONFIG=/config/config.json
- XDG_CACHE_HOME=/cache
- TZ=\${TZ}
volumes:
- backrest-testhash/data:/data
- backrest-testhash/config:/config
- backrest-testhash/cache:/cache
- /:/userdata:ro
volumes:
backrest-testhash:
backrest-cache-testhash:
`) as ComposeSpecification;
test("Should handle volume paths with subdirectories correctly", () => {
const composeData = load(composeFileBackrest) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerComposeBackrest);
});

View File

@@ -27,6 +27,11 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
giteaBranch: "",
giteaBuildPath: "",
giteaId: "",
giteaOwner: "",
giteaRepository: "",
cleanCache: false,
watchPaths: [],
applicationStatus: "done",

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { Schema } from "@dokploy/server/templates";
import type { CompleteTemplate } from "@dokploy/server/templates/processors";
import { processTemplate } from "@dokploy/server/templates/processors";
import type { Schema } from "@dokploy/server/templates";
import { describe, expect, it } from "vitest";
describe("processTemplate", () => {
// Mock schema for testing
@@ -233,6 +233,49 @@ describe("processTemplate", () => {
expect(base64Value.length).toBeGreaterThanOrEqual(42);
expect(base64Value.length).toBeLessThanOrEqual(44);
});
it("should handle boolean values in env vars when provided as an array", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: [
"ENABLE_USER_SIGN_UP=false",
"DEBUG_MODE=true",
"SOME_NUMBER=42",
],
mounts: [],
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(3);
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
expect(result.envs).toContain("DEBUG_MODE=true");
expect(result.envs).toContain("SOME_NUMBER=42");
});
it("should handle boolean values in env vars when provided as an object", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: {
ENABLE_USER_SIGN_UP: false,
DEBUG_MODE: true,
SOME_NUMBER: 42,
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(3);
expect(result.envs).toContain("ENABLE_USER_SIGN_UP=false");
expect(result.envs).toContain("DEBUG_MODE=true");
expect(result.envs).toContain("SOME_NUMBER=42");
});
});
describe("mounts processing", () => {

View File

@@ -7,6 +7,11 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
giteaRepository: "",
giteaOwner: "",
giteaBranch: "",
giteaBuildPath: "",
giteaId: "",
cleanCache: false,
applicationStatus: "done",
appName: "",

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
@@ -32,7 +33,6 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
const ImportSchema = z.object({
base64: z.string(),

View File

@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
enum BuildType {
export enum BuildType {
dockerfile = "dockerfile",
heroku_buildpacks = "heroku_buildpacks",
paketo_buildpacks = "paketo_buildpacks",
@@ -29,9 +29,18 @@ enum BuildType {
railpack = "railpack",
}
const buildTypeDisplayMap: Record<BuildType, string> = {
[BuildType.dockerfile]: "Dockerfile",
[BuildType.railpack]: "Railpack",
[BuildType.nixpacks]: "Nixpacks",
[BuildType.heroku_buildpacks]: "Heroku Buildpacks",
[BuildType.paketo_buildpacks]: "Paketo Buildpacks",
[BuildType.static]: "Static",
};
const mySchema = z.discriminatedUnion("buildType", [
z.object({
buildType: z.literal("dockerfile"),
buildType: z.literal(BuildType.dockerfile),
dockerfile: z
.string({
required_error: "Dockerfile path is required",
@@ -42,39 +51,88 @@ const mySchema = z.discriminatedUnion("buildType", [
dockerBuildStage: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
buildType: z.literal(BuildType.heroku_buildpacks),
herokuVersion: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("paketo_buildpacks"),
buildType: z.literal(BuildType.paketo_buildpacks),
}),
z.object({
buildType: z.literal("nixpacks"),
buildType: z.literal(BuildType.nixpacks),
publishDirectory: z.string().optional(),
}),
z.object({
buildType: z.literal("static"),
buildType: z.literal(BuildType.static),
}),
z.object({
buildType: z.literal("railpack"),
buildType: z.literal(BuildType.railpack),
}),
]);
type AddTemplate = z.infer<typeof mySchema>;
interface Props {
applicationId: string;
}
interface ApplicationData {
buildType: BuildType;
dockerfile?: string | null;
dockerContextPath?: string | null;
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
}
function isValidBuildType(value: string): value is BuildType {
return Object.values(BuildType).includes(value as BuildType);
}
const resetData = (data: ApplicationData): AddTemplate => {
switch (data.buildType) {
case BuildType.dockerfile:
return {
buildType: BuildType.dockerfile,
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
dockerBuildStage: data.dockerBuildStage || "",
};
case BuildType.heroku_buildpacks:
return {
buildType: BuildType.heroku_buildpacks,
herokuVersion: data.herokuVersion || "",
};
case BuildType.nixpacks:
return {
buildType: BuildType.nixpacks,
publishDirectory: data.publishDirectory || undefined,
};
case BuildType.paketo_buildpacks:
return {
buildType: BuildType.paketo_buildpacks,
};
case BuildType.static:
return {
buildType: BuildType.static,
};
case BuildType.railpack:
return {
buildType: BuildType.railpack,
};
default:
const buildType = data.buildType as BuildType;
return {
buildType,
} as AddTemplate;
}
};
export const ShowBuildChooseForm = ({ applicationId }: Props) => {
const { mutateAsync, isLoading } =
api.application.saveBuildType.useMutation();
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
{ applicationId },
{ enabled: !!applicationId },
);
const form = useForm<AddTemplate>({
@@ -85,46 +143,36 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
});
const buildType = form.watch("buildType");
useEffect(() => {
if (data) {
if (data.buildType === "dockerfile") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
dockerBuildStage: data.dockerBuildStage || "",
}),
});
} else if (data.buildType === "heroku_buildpacks") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
herokuVersion: data.herokuVersion || "",
}),
});
} else {
form.reset({
buildType: data.buildType,
publishDirectory: data.publishDirectory || undefined,
});
}
const typedData: ApplicationData = {
...data,
buildType: isValidBuildType(data.buildType)
? (data.buildType as BuildType)
: BuildType.nixpacks, // fallback
};
form.reset(resetData(typedData));
}
}, [form.formState.isSubmitSuccessful, form.reset, data, form]);
}, [data, form]);
const onSubmit = async (data: AddTemplate) => {
await mutateAsync({
applicationId,
buildType: data.buildType,
publishDirectory:
data.buildType === "nixpacks" ? data.publishDirectory : null,
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
data.buildType === BuildType.nixpacks ? data.publishDirectory : null,
dockerfile:
data.buildType === BuildType.dockerfile ? data.dockerfile : null,
dockerContextPath:
data.buildType === "dockerfile" ? data.dockerContextPath : null,
data.buildType === BuildType.dockerfile ? data.dockerContextPath : null,
dockerBuildStage:
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
data.buildType === BuildType.dockerfile ? data.dockerBuildStage : null,
herokuVersion:
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion
: null,
})
.then(async () => {
toast.success("Build type saved");
@@ -160,193 +208,143 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
control={form.control}
name="buildType"
defaultValue={form.control._defaultValues.buildType}
render={({ field }) => {
return (
<FormItem className="space-y-3">
<FormLabel>Build Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dockerfile" />
</FormControl>
<FormLabel className="font-normal">
Dockerfile
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="railpack" />
</FormControl>
<FormLabel className="font-normal">
Railpack{" "}
<Badge className="ml-1 text-xs px-1">New</Badge>
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="nixpacks" />
</FormControl>
<FormLabel className="font-normal">
Nixpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="heroku_buildpacks" />
</FormControl>
<FormLabel className="font-normal">
Heroku Buildpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="paketo_buildpacks" />
</FormControl>
<FormLabel className="font-normal">
Paketo Buildpacks
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="static" />
</FormControl>
<FormLabel className="font-normal">Static</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
);
}}
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Build Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-col space-y-1"
>
{Object.entries(buildTypeDisplayMap).map(
([value, label]) => (
<FormItem
key={value}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={value} />
</FormControl>
<FormLabel className="font-normal">
{label}
{value === BuildType.railpack && (
<Badge className="ml-2 px-1 text-xs">New</Badge>
)}
</FormLabel>
</FormItem>
),
)}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{buildType === "heroku_buildpacks" && (
{buildType === BuildType.heroku_buildpacks && (
<FormField
control={form.control}
name="herokuVersion"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Heroku Version (Optional)</FormLabel>
<FormControl>
<Input
placeholder={"Heroku Version (Default: 24)"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
render={({ field }) => (
<FormItem>
<FormLabel>Heroku Version (Optional)</FormLabel>
<FormControl>
<Input
placeholder="Heroku Version (Default: 24)"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{buildType === "dockerfile" && (
{buildType === BuildType.dockerfile && (
<>
<FormField
control={form.control}
name="dockerfile"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder={"Path of your docker file"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="dockerContextPath"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Docker Context Path</FormLabel>
<FormControl>
<Input
placeholder={
"Path of your docker context default: ."
}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="dockerBuildStage"
render={({ field }) => {
return (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Docker Build Stage</FormLabel>
<FormDescription>
Allows you to target a specific stage in a
Multi-stage Dockerfile. If empty, Docker defaults to
build the last defined stage.
</FormDescription>
</div>
<FormControl>
<Input
placeholder={"E.g. production"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
);
}}
/>
</>
)}
{buildType === "nixpacks" && (
<FormField
control={form.control}
name="publishDirectory"
render={({ field }) => {
return (
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets
should be served as a static site.
</FormDescription>
</div>
<FormLabel>Docker File</FormLabel>
<FormControl>
<Input
placeholder={"Publish Directory"}
placeholder="Path of your docker file"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
)}
/>
<FormField
control={form.control}
name="dockerContextPath"
render={({ field }) => (
<FormItem>
<FormLabel>Docker Context Path</FormLabel>
<FormControl>
<Input
placeholder="Path of your docker context (default: .)"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dockerBuildStage"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Docker Build Stage</FormLabel>
<FormDescription>
Allows you to target a specific stage in a Multi-stage
Dockerfile. If empty, Docker defaults to build the
last defined stage.
</FormDescription>
</div>
<FormControl>
<Input
placeholder="E.g. production"
{...field}
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
{buildType === BuildType.nixpacks && (
<FormField
control={form.control}
name="publishDirectory"
render={({ field }) => (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Publish Directory</FormLabel>
<FormDescription>
Allows you to serve a single directory via NGINX after
the build phase. Useful if the final build assets should
be served as a static site.
</FormDescription>
</div>
<FormControl>
<Input
placeholder="Publish Directory"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">

View File

@@ -41,8 +41,8 @@ import { toast } from "sonner";
import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import type z from "zod";
import Link from "next/link";
import type z from "zod";
type Domain = z.infer<typeof domain>;

View File

@@ -4,10 +4,10 @@ import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { useEffect } from "react";
const addEnvironmentSchema = z.object({
env: z.string(),

View File

@@ -1,4 +1,6 @@
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const BitbucketProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -26,15 +26,15 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import { useRouter } from "next/router";
import Link from "next/link";
import { useRouter } from "next/router";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { GitIcon } from "@/components/icons/data-tools-icons";
const GitProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -0,0 +1,515 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface GiteaRepository {
name: string;
url: string;
id: number;
owner: {
username: string;
};
}
interface GiteaBranch {
name: string;
commit: {
id: string;
};
}
const GiteaProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveGiteaProvider = ({ applicationId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
api.application.saveGiteaProvider.useMutation();
const form = useForm<GiteaProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
},
giteaId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GiteaProviderSchema),
});
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
{ giteaId },
{
enabled: !!giteaId,
},
);
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitea.getGiteaRepositories.useQuery(
{
giteaId,
},
{
enabled: !!giteaId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitea.getGiteaBranches.useQuery(
{
owner: repository?.owner,
repositoryName: repository?.repo,
giteaId: giteaId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.giteaBranch || "",
repository: {
repo: data.giteaRepository || "",
owner: data.giteaOwner || "",
},
buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
giteaBranch: data.branch,
giteaRepository: data.repository.repo,
giteaOwner: data.repository.owner,
giteaBuildPath: data.buildPath,
giteaId: data.giteaId,
applicationId,
watchPaths: data.watchPaths,
})
.then(async () => {
toast.success("Service Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Gitea provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="giteaId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitea Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitea Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{giteaProviders?.map((giteaProvider) => (
<SelectItem
key={giteaProvider.giteaId}
value={giteaProvider.giteaId}
>
{giteaProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
>
<GiteaIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo: GiteaRepository) =>
repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories && repositories.length === 0 && (
<CommandEmpty>
No repositories found.
</CommandEmpty>
)}
{repositories?.map((repo: GiteaRepository) => {
return (
<CommandItem
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch: GiteaBranch) =>
branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch: GiteaBranch) => (
<CommandItem
value={branch.name}
key={branch.commit.id}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path: string, index: number) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1"
>
{path}
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newPaths = [...field.value];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
input.value = "";
}
}
}}
/>
</FormControl>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...field.value, path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGiteaProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,3 +1,5 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -34,17 +36,15 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import Link from "next/link";
import { GithubIcon } from "@/components/icons/data-tools-icons";
const GithubProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,4 +1,6 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -35,17 +37,15 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import Link from "next/link";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
const GitlabProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,10 +1,12 @@
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
import {
BitbucketIcon,
DockerIcon,
GitIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -18,7 +20,14 @@ import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider";
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
type TabState =
| "github"
| "docker"
| "git"
| "drop"
| "gitlab"
| "bitbucket"
| "gitea";
interface Props {
applicationId: string;
@@ -29,6 +38,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data: application } = api.application.one.useQuery({ applicationId });
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
@@ -78,6 +88,13 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="gitea"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GiteaIcon className="size-4 text-current fill-current" />
Gitea
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -162,6 +179,26 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
</div>
)}
</TabsContent>
<TabsContent value="gitea" className="w-full p-2">
{giteaProviders && giteaProviders?.length > 0 ? (
<SaveGiteaProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GiteaIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Gitea, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="docker" className="w-full p-2">
<SaveDockerProvider applicationId={applicationId} />
</TabsContent>

View File

@@ -41,8 +41,8 @@ import {
import { domainCompose } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import type z from "zod";
import Link from "next/link";
import type z from "zod";
type Domain = z.infer<typeof domainCompose>;

View File

@@ -1,4 +1,6 @@
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const BitbucketProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,3 +1,4 @@
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -27,13 +28,12 @@ import {
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GitProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -0,0 +1,483 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GiteaProviderSchema = z.object({
composePath: z.string().min(1),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
});
type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
interface Props {
composeId: string;
}
export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGiteaProvider } =
api.compose.update.useMutation();
const form = useForm<GiteaProvider>({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
owner: "",
repo: "",
},
giteaId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GiteaProviderSchema),
});
const repository = form.watch("repository");
const giteaId = form.watch("giteaId");
const { data: giteaUrl } = api.gitea.getGiteaUrl.useQuery(
{ giteaId },
{
enabled: !!giteaId,
},
);
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitea.getGiteaRepositories.useQuery<Repository[]>(
{
giteaId,
},
{
enabled: !!giteaId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitea.getGiteaBranches.useQuery(
{
owner: repository?.owner,
repositoryName: repository?.repo,
giteaId: giteaId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!giteaId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.giteaBranch || "",
repository: {
repo: data.giteaRepository || "",
owner: data.giteaOwner || "",
},
composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
giteaBranch: data.branch,
giteaRepository: data.repository.repo,
giteaOwner: data.repository.owner,
composePath: data.composePath,
giteaId: data.giteaId,
composeId,
sourceType: "gitea",
composeStatus: "idle",
watchPaths: data.watchPaths,
} as any)
.then(async () => {
toast.success("Service Provider Saved");
await refetch();
})
.catch(() => {
toast.error("Error saving the Gitea provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="giteaId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitea Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitea Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{giteaProviders?.map((giteaProvider) => (
<SelectItem
key={giteaProvider.giteaId}
value={giteaProvider.giteaId}
>
{giteaProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && (
<Link
href={`${giteaUrl}/${field.value.owner}/${field.value.repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
>
<GiteaIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
key={repo.url}
value={repo.name}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branches..."
className="h-9"
/>
<CommandEmpty>No branches found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
key={branch.name}
value={branch.name}
onSelect={() =>
form.setValue("branch", branch.name)
}
>
<span className="flex items-center gap-2">
{branch.name}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.branch && (
<p className={cn("text-sm font-medium text-destructive")}>
Branch is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="composePath"
render={({ field }) => (
<FormItem>
<FormLabel>Compose Path</FormLabel>
<FormControl>
<Input placeholder="docker-compose.yml" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}
}}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button type="submit" isLoading={isSavingGiteaProvider}>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -39,12 +40,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GithubProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,4 +1,6 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -39,13 +41,11 @@ import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import Link from "next/link";
const GitlabProviderSchema = z.object({
composePath: z.string().min(1),

View File

@@ -1,6 +1,7 @@
import {
BitbucketIcon,
GitIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -14,10 +15,11 @@ import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props {
composeId: string;
}
@@ -27,9 +29,11 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
const { data: compose } = api.compose.one.useQuery({ composeId });
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>
@@ -54,21 +58,21 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GithubIcon className="size-4 text-current fill-current" />
Github
GitHub
</TabsTrigger>
<TabsTrigger
value="gitlab"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitlabIcon className="size-4 text-current fill-current" />
Gitlab
GitLab
</TabsTrigger>
<TabsTrigger
value="bitbucket"
@@ -77,7 +81,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="gitea"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GiteaIcon className="size-4 text-current fill-current" /> Gitea
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
@@ -89,11 +98,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
value="raw"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<CodeIcon className="size-4 " />
<CodeIcon className="size-4" />
Raw
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="github" className="w-full p-2">
{githubProviders && githubProviders?.length > 0 ? (
<SaveGithubProviderCompose composeId={composeId} />
@@ -154,6 +164,26 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
</div>
)}
</TabsContent>
<TabsContent value="gitea" className="w-full p-2">
{giteaProviders && giteaProviders?.length > 0 ? (
<SaveGiteaProviderCompose composeId={composeId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GiteaIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Gitea, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="git" className="w-full p-2">
<SaveGitProviderCompose composeId={composeId} />
</TabsContent>

View File

@@ -61,7 +61,7 @@ type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
interface Props {
databaseId: string;
databaseType: "postgres" | "mariadb" | "mysql" | "mongo";
databaseType: "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
refetch: () => void;
}
@@ -85,7 +85,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
useEffect(() => {
form.reset({
database: "",
database: databaseType === "web-server" ? "dokploy" : "",
destinationId: "",
enabled: true,
prefix: "/",
@@ -112,7 +112,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
? {
mongoId: databaseId,
}
: undefined;
: databaseType === "web-server"
? {
userId: databaseId,
}
: undefined;
await createBackup({
destinationId: data.destinationId,
@@ -236,7 +240,11 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormItem>
<FormLabel>Database</FormLabel>
<FormControl>
<Input placeholder={"dokploy"} {...field} />
<Input
disabled={databaseType === "web-server"}
placeholder={"dokploy"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -1,3 +1,5 @@
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -23,6 +25,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
@@ -32,23 +35,20 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { debounce } from "lodash";
import { Input } from "@/components/ui/input";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
interface Props {
databaseId: string;
databaseType: Exclude<ServiceType, "application" | "redis">;
serverId: string | null;
databaseType: Exclude<ServiceType, "application" | "redis"> | "web-server";
serverId?: string | null;
}
const RestoreBackupSchema = z.object({
@@ -91,7 +91,7 @@ export const RestoreBackup = ({
defaultValues: {
destinationId: "",
backupFile: "",
databaseName: "",
databaseName: databaseType === "web-server" ? "dokploy" : "",
},
resolver: zodResolver(RestoreBackupSchema),
});
@@ -340,7 +340,11 @@ export const RestoreBackup = ({
<FormItem className="">
<FormLabel>Database Name</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter database name" />
<Input
disabled={databaseType === "web-server"}
{...field}
placeholder="Enter database name"
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -14,18 +14,18 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { AddBackup } from "./add-backup";
import { UpdateBackup } from "./update-backup";
import { RestoreBackup } from "./restore-backup";
import { useState } from "react";
import { UpdateBackup } from "./update-backup";
interface Props {
id: string;
type: Exclude<ServiceType, "application" | "redis">;
type: Exclude<ServiceType, "application" | "redis"> | "web-server";
}
export const ShowBackups = ({ id, type }: Props) => {
const [activeManualBackup, setActiveManualBackup] = useState<
@@ -38,6 +38,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
"web-server": () => api.user.getBackups.useQuery(),
};
const { data } = api.destination.all.useQuery();
const { data: postgres, refetch } = queryMap[type]
@@ -49,6 +50,7 @@ export const ShowBackups = ({ id, type }: Props) => {
mysql: () => api.backup.manualBackupMySql.useMutation(),
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
mongo: () => api.backup.manualBackupMongo.useMutation(),
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
};
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
@@ -64,7 +66,10 @@ export const ShowBackups = ({ id, type }: Props) => {
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
<div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle>
<CardTitle className="text-xl flex flex-row gap-2">
<Database className="size-6 text-muted-foreground" />
Backups
</CardTitle>
<CardDescription>
Add backups to your database to save the data to a different
provider.
@@ -73,11 +78,17 @@ export const ShowBackups = ({ id, type }: Props) => {
{postgres && postgres?.backups?.length > 0 && (
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
{type !== "web-server" && (
<AddBackup
databaseId={id}
databaseType={type}
refetch={refetch}
/>
)}
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
serverId={"serverId" in postgres ? postgres.serverId : undefined}
/>
</div>
)}
@@ -115,7 +126,9 @@ export const ShowBackups = ({ id, type }: Props) => {
<RestoreBackup
databaseId={id}
databaseType={type}
serverId={postgres.serverId}
serverId={
"serverId" in postgres ? postgres.serverId : undefined
}
/>
</div>
</div>

View File

@@ -67,7 +67,7 @@ import {
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";

View File

@@ -13,7 +13,6 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { useState } from "react";
const examples = [
"Make a personal blog",
@@ -23,7 +22,7 @@ const examples = [
"Sendgrid service opensource analogue",
];
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery();

View File

@@ -0,0 +1,172 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
export type Services = {
appName: string;
serverId?: string | null;
name: string;
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "mongo"
| "redis"
| "compose";
description?: string | null;
id: string;
createdAt: string;
status?: "idle" | "running" | "done" | "error";
};
interface DuplicateProjectProps {
projectId: string;
services: Services[];
selectedServiceIds: string[];
}
export const DuplicateProject = ({
projectId,
services,
selectedServiceIds,
}: DuplicateProjectProps) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const utils = api.useUtils();
const router = useRouter();
const selectedServices = services.filter((service) =>
selectedServiceIds.includes(service.id),
);
const { mutateAsync: duplicateProject, isLoading } =
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
toast.success("Project duplicated successfully");
setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`);
},
onError: (error) => {
toast.error(error.message);
},
});
const handleDuplicate = async () => {
if (!name) {
toast.error("Project name is required");
return;
}
await duplicateProject({
sourceProjectId: projectId,
name,
description,
includeServices: true,
selectedServices: selectedServices.map((service) => ({
id: service.id,
type: service.type,
})),
});
};
return (
<Dialog
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen);
if (!isOpen) {
// Reset form when closing
setName("");
setDescription("");
}
}}
>
<DialogTrigger asChild>
<Button variant="ghost" className="w-full justify-start">
<Copy className="mr-2 h-4 w-4" />
Duplicate
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle>
<DialogDescription>
Create a new project with the selected services
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
{selectedServices.map((service) => (
<div key={service.id} className="flex items-center space-x-2">
<span className="text-sm">
{service.name} ({service.type})
</span>
</div>
))}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setOpen(false)}
disabled={isLoading}
>
Cancel
</Button>
<Button onClick={handleDuplicate} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating...
</>
) : (
"Duplicate"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { api } from "@/utils/api";
import {
Area,
AreaChart,

View File

@@ -25,13 +25,13 @@ import {
import { type RouterOutputs, api } from "@/utils/api";
import { format } from "date-fns";
import {
ArrowDownUp,
AlertCircle,
InfoIcon,
ArrowDownUp,
Calendar as CalendarIcon,
InfoIcon,
} from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { RequestDistributionChart } from "./request-distribution-chart";
import { RequestsTable } from "./requests-table";

View File

@@ -1,14 +1,22 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
@@ -17,22 +25,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import copy from "copy-to-clipboard";
import { CodeEditor } from "@/components/shared/code-editor";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),

View File

@@ -1,3 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -7,13 +9,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { ExternalLinkIcon, KeyIcon, Trash2, Clock, Tag } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { Clock, ExternalLinkIcon, KeyIcon, Tag, Trash2 } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { formatDistanceToNow } from "date-fns";
import { DialogAction } from "@/components/shared/dialog-action";
import { AddApiKey } from "./add-api-key";
import { Badge } from "@/components/ui/badge";
export const ShowApiKeys = () => {
const { data, refetch } = api.user.get.useQuery();

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -12,7 +13,6 @@ import { ExternalLink, PlusIcon } from "lucide-react";
import Link from "next/link";
import { AddManager } from "./manager/add-manager";
import { AddWorker } from "./workers/add-worker";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props {
serverId?: string;

View File

@@ -35,9 +35,9 @@ import { api } from "@/utils/api";
import {
Boxes,
HelpCircle,
Loader2,
LockIcon,
MoreHorizontal,
Loader2,
} from "lucide-react";
import { toast } from "sonner";
import { AddNode } from "./add-node";

View File

@@ -0,0 +1,286 @@
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import {
type GiteaProviderResponse,
getGiteaOAuthUrl,
} from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
giteaUrl: z.string().min(1, {
message: "Gitea URL is required",
}),
clientId: z.string().min(1, {
message: "Client ID is required",
}),
clientSecret: z.string().min(1, {
message: "Client Secret is required",
}),
redirectUri: z.string().min(1, {
message: "Redirect URI is required",
}),
organizationName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddGiteaProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const urlObj = useUrl();
const baseUrl =
typeof urlObj === "string" ? urlObj : (urlObj as any)?.url || "";
const { mutateAsync, error, isError } = api.gitea.create.useMutation();
const webhookUrl = `${baseUrl}/api/providers/gitea/callback`;
const form = useForm<Schema>({
defaultValues: {
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
},
resolver: zodResolver(Schema),
});
const giteaUrl = form.watch("giteaUrl");
useEffect(() => {
form.reset({
clientId: "",
clientSecret: "",
redirectUri: webhookUrl,
name: "",
giteaUrl: "https://gitea.com",
});
}, [form, webhookUrl, isOpen]);
const onSubmit = async (data: Schema) => {
try {
// Send the form data to create the Gitea provider
const result = (await mutateAsync({
clientId: data.clientId,
clientSecret: data.clientSecret,
name: data.name,
redirectUri: data.redirectUri,
giteaUrl: data.giteaUrl,
organizationName: data.organizationName,
})) as unknown as GiteaProviderResponse;
// Check if we have a giteaId from the response
if (!result || !result.giteaId) {
toast.error("Failed to get Gitea ID from response");
return;
}
// Generate OAuth URL using the shared utility
const authUrl = getGiteaOAuthUrl(
result.giteaId,
data.clientId,
data.giteaUrl,
baseUrl,
);
// Open the Gitea OAuth URL
if (authUrl !== "#") {
window.open(authUrl, "_blank");
} else {
toast.error("Configuration Incomplete", {
description: "Please fill in Client ID and Gitea URL first.",
});
}
toast.success("Gitea provider created successfully");
setIsOpen(false);
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(`Error configuring Gitea: ${error.message}`);
} else {
toast.error("An unknown error occurred.");
}
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="flex items-center space-x-1 bg-green-700 text-white hover:bg-green-500"
>
<GiteaIcon />
<span>Gitea</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Gitea Provider <GiteaIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-gitea"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your Gitea account, you need to create a new
application in your Gitea settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Go to your Gitea settings{" "}
<Link
href={`${giteaUrl}/user/settings/applications`}
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
Navigate to Applications {"->"} Create new OAuth2
Application
</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>
Redirect URI:{" "}
<span className="text-primary">{webhookUrl}</span>{" "}
</li>
</ul>
</li>
<li>
After creating, you'll receive an ID and Secret, copy them
and paste them below.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.com/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirectUri"
render={({ field }) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input
disabled
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure Gitea App
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,296 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { getGiteaOAuthUrl } from "@/utils/gitea-utils";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
giteaUrl: z.string().min(1, "Gitea URL is required"),
clientId: z.string().min(1, "Client ID is required"),
clientSecret: z.string().min(1, "Client Secret is required"),
});
interface Props {
giteaId: string;
}
export const EditGiteaProvider = ({ giteaId }: Props) => {
const router = useRouter();
const [open, setOpen] = useState(false);
const {
data: gitea,
isLoading,
refetch,
} = api.gitea.one.useQuery({ giteaId });
const { mutateAsync, isLoading: isUpdating } = api.gitea.update.useMutation();
const { mutateAsync: testConnection, isLoading: isTesting } =
api.gitea.testConnection.useMutation();
const url = useUrl();
const utils = api.useUtils();
useEffect(() => {
const { connected, error } = router.query;
if (!router.isReady) return;
if (connected) {
toast.success("Successfully connected to Gitea", {
description: "Your Gitea provider has been authorized.",
id: "gitea-connection-success",
});
refetch();
router.replace(
{
pathname: router.pathname,
query: {},
},
undefined,
{ shallow: true },
);
}
if (error) {
toast.error("Gitea Connection Failed", {
description: decodeURIComponent(error as string),
id: "gitea-connection-error",
});
router.replace(
{
pathname: router.pathname,
query: {},
},
undefined,
{ shallow: true },
);
}
}, [router.query, router.isReady, refetch]);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
giteaUrl: "https://gitea.com",
clientId: "",
clientSecret: "",
},
});
useEffect(() => {
if (gitea) {
form.reset({
name: gitea.gitProvider?.name || "",
giteaUrl: gitea.giteaUrl || "https://gitea.com",
clientId: gitea.clientId || "",
clientSecret: gitea.clientSecret || "",
});
}
}, [gitea, form]);
const onSubmit = async (values: z.infer<typeof formSchema>) => {
await mutateAsync({
giteaId: giteaId,
gitProviderId: gitea?.gitProvider?.gitProviderId || "",
name: values.name,
giteaUrl: values.giteaUrl,
clientId: values.clientId,
clientSecret: values.clientSecret,
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Gitea provider updated successfully");
await refetch();
setOpen(false);
})
.catch(() => {
toast.error("Error updating Gitea provider");
});
};
const handleTestConnection = async () => {
try {
const result = await testConnection({ giteaId });
toast.success("Gitea Connection Verified", {
description: result,
});
} catch (error: any) {
const formValues = form.getValues();
const authUrl =
error.authorizationUrl ||
getGiteaOAuthUrl(
giteaId,
formValues.clientId,
formValues.giteaUrl,
typeof url === "string" ? url : (url as any).url || "",
);
toast.error("Gitea Not Connected", {
description:
error.message || "Please complete the OAuth authorization process.",
action:
authUrl && authUrl !== "#"
? {
label: "Authorize Now",
onClick: () => window.open(authUrl, "_blank"),
}
: undefined,
});
}
};
if (isLoading) {
return (
<Button variant="ghost" size="icon" disabled>
<PenBoxIcon className="h-4 w-4 text-muted-foreground" />
</Button>
);
}
// Function to handle dialog open state
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Gitea Provider</DialogTitle>
<DialogDescription>
Update your Gitea provider details.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="My Gitea"
{...field}
autoFocus={false}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="giteaUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gitea URL</FormLabel>
<FormControl>
<Input placeholder="https://gitea.example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>Client ID</FormLabel>
<FormControl>
<Input placeholder="Client ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Client Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
isLoading={isTesting}
>
Test Connection
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
const formValues = form.getValues();
const authUrl = getGiteaOAuthUrl(
giteaId,
formValues.clientId,
formValues.giteaUrl,
typeof url === "string" ? url : (url as any).url || "",
);
if (authUrl !== "#") {
window.open(authUrl, "_blank");
}
}}
>
Connect to Gitea
</Button>
<Button type="submit" isLoading={isUpdating}>
Save
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,5 +1,6 @@
import {
BitbucketIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
@@ -26,6 +27,8 @@ import Link from "next/link";
import { toast } from "sonner";
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
import { AddGiteaProvider } from "./gitea/add-gitea-provider";
import { EditGiteaProvider } from "./gitea/edit-gitea-provider";
import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
@@ -36,19 +39,18 @@ export const ShowGitProviders = () => {
const { mutateAsync, isLoading: isRemoving } =
api.gitProvider.remove.useMutation();
const url = useUrl();
const getGitlabUrl = (
clientId: string,
gitlabId: string,
gitlabUrl: string,
) => {
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
const scope = "api read_user read_repository";
const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
return authUrl;
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
@@ -82,6 +84,7 @@ export const ShowGitProviders = () => {
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
<AddGiteaProvider />
</div>
</div>
</div>
@@ -97,6 +100,7 @@ export const ShowGitProviders = () => {
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
<AddGiteaProvider />
</div>
</div>
</div>
@@ -107,13 +111,16 @@ export const ShowGitProviders = () => {
const isGitlab = gitProvider.providerType === "gitlab";
const isBitbucket =
gitProvider.providerType === "bitbucket";
const isGitea = gitProvider.providerType === "gitea";
const haveGithubRequirements =
gitProvider.providerType === "github" &&
isGithub &&
gitProvider.github?.githubPrivateKey &&
gitProvider.github?.githubAppId &&
gitProvider.github?.githubInstallationId;
const haveGitlabRequirements =
isGitlab &&
gitProvider.gitlab?.accessToken &&
gitProvider.gitlab?.refreshToken;
@@ -122,18 +129,19 @@ export const ShowGitProviders = () => {
key={gitProvider.gitProviderId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
>
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col items-center justify-between">
<div className="flex gap-2 flex-row items-center">
{gitProvider.providerType === "github" && (
{isGithub && (
<GithubIcon className="size-5" />
)}
{gitProvider.providerType === "gitlab" && (
{isGitlab && (
<GitlabIcon className="size-5" />
)}
{gitProvider.providerType === "bitbucket" && (
{isBitbucket && (
<BitbucketIcon className="size-5" />
)}
{isGitea && <GiteaIcon className="size-5" />}
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">
{gitProvider.name}
@@ -194,26 +202,33 @@ export const ShowGitProviders = () => {
</Link>
</div>
)}
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github.githubId}
githubId={gitProvider.github?.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab.gitlabId}
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket.bitbucketId
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isGitea && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
/>
)}
<DialogAction
title="Delete Git Provider"
description="Are you sure you want to delete this Git Provider?"
@@ -238,7 +253,7 @@ export const ShowGitProviders = () => {
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />

View File

@@ -25,7 +25,7 @@ export const SecurityAudit = ({ serverId }: Props) => {
enabled: !!serverId,
},
);
const _utils = api.useUtils();
return (
<CardContent className="p-0">
<div className="flex flex-col gap-4">
@@ -36,10 +36,12 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="flex items-center gap-2">
<LockKeyhole className="size-5" />
<CardTitle className="text-xl">
Setup Security Sugestions
Setup Security Suggestions
</CardTitle>
</div>
<CardDescription>Check the security sugestions</CardDescription>
<CardDescription>
Check the security suggestions
</CardDescription>
</div>
<Button
isLoading={isRefreshing}
@@ -120,36 +122,36 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="grid gap-2.5">
<StatusRow
label="Enabled"
isEnabled={data?.ssh.enabled}
isEnabled={data?.ssh?.enabled}
description={
data?.ssh.enabled
data?.ssh?.enabled
? "Enabled"
: "Not Enabled (SSH should be enabled)"
}
/>
<StatusRow
label="Key Auth"
isEnabled={data?.ssh.keyAuth}
isEnabled={data?.ssh?.keyAuth}
description={
data?.ssh.keyAuth
data?.ssh?.keyAuth
? "Enabled (Recommended)"
: "Not Enabled (Key Authentication should be enabled)"
}
/>
<StatusRow
label="Password Auth"
isEnabled={data?.ssh.passwordAuth === "no"}
isEnabled={data?.ssh?.passwordAuth === "no"}
description={
data?.ssh.passwordAuth === "no"
data?.ssh?.passwordAuth === "no"
? "Disabled (Recommended)"
: "Enabled (Password Authentication should be disabled)"
}
/>
<StatusRow
label="Use PAM"
isEnabled={data?.ssh.usePam === "no"}
isEnabled={data?.ssh?.usePam === "no"}
description={
data?.ssh.usePam === "no"
data?.ssh?.usePam === "no"
? "Disabled (Recommended for key-based auth)"
: "Enabled (Should be disabled when using key-based auth)"
}
@@ -166,9 +168,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
<div className="grid gap-2.5">
<StatusRow
label="Installed"
isEnabled={data?.fail2ban.installed}
isEnabled={data?.fail2ban?.installed}
description={
data?.fail2ban.installed
data?.fail2ban?.installed
? "Installed (Recommended)"
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
}
@@ -176,18 +178,18 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="Enabled"
isEnabled={data?.fail2ban.enabled}
isEnabled={data?.fail2ban?.enabled}
description={
data?.fail2ban.enabled
data?.fail2ban?.enabled
? "Enabled (Recommended)"
: "Not Enabled (Fail2Ban service should be enabled)"
}
/>
<StatusRow
label="Active"
isEnabled={data?.fail2ban.active}
isEnabled={data?.fail2ban?.active}
description={
data?.fail2ban.active
data?.fail2ban?.active
? "Active (Recommended)"
: "Not Active (Fail2Ban service should be running)"
}
@@ -195,9 +197,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="SSH Protection"
isEnabled={data?.fail2ban.sshEnabled === "true"}
isEnabled={data?.fail2ban?.sshEnabled === "true"}
description={
data?.fail2ban.sshEnabled === "true"
data?.fail2ban?.sshEnabled === "true"
? "Enabled (Recommended)"
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
}
@@ -205,11 +207,11 @@ export const SecurityAudit = ({ serverId }: Props) => {
<StatusRow
label="SSH Mode"
isEnabled={data?.fail2ban.sshMode === "aggressive"}
isEnabled={data?.fail2ban?.sshMode === "aggressive"}
description={
data?.fail2ban.sshMode === "aggressive"
data?.fail2ban?.sshMode === "aggressive"
? "Aggressive Mode (Recommended)"
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
: `Mode: ${data?.fail2ban?.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
}
/>
</div>

View File

@@ -33,6 +33,7 @@ import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions";
import { HandleServers } from "./handle-servers";
@@ -42,7 +43,6 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
export const ShowServers = () => {
const { t } = useTranslation("settings");

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
@@ -22,7 +23,6 @@ import dynamic from "next/dynamic";
import type React from "react";
import { useEffect, useState } from "react";
import { badgeStateColor } from "../../application/logs/show";
import { Badge } from "@/components/ui/badge";
export const DockerLogsId = dynamic(
() =>

View File

@@ -12,7 +12,7 @@ import {
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { DatabaseIcon, AlertTriangle } from "lucide-react";
import { AlertTriangle, DatabaseIcon } from "lucide-react";
import { toast } from "sonner";
interface Props {

View File

@@ -1,6 +1,6 @@
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { RebuildDatabase } from "./rebuild-database";
interface Props {

View File

@@ -238,6 +238,41 @@ export const BitbucketIcon = ({ className }: Props) => {
);
};
export const GiteaIcon = ({ className }: Props) => {
return (
<svg
className={className}
version="1.1"
id="main_outline"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="5.67 143.05 628.65 387.55"
enableBackground="new 0 0 640 640"
>
<g>
<path
id="teabag"
style={{ fill: "#FFFFFF" }}
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"
/>
<g>
<g>
<path
style={{ fill: "#609926" }}
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"
/>
<path
style={{ fill: "#609926" }}
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8C343.2,346.5,335,363.3,326.8,380.1z"
/>
</g>
</g>
</g>
</svg>
);
};
export const DockerIcon = ({ className }: Props) => {
return (
<svg

View File

@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index";
import { Download } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import UpdateServer from "../dashboard/settings/web-server/update-server";
import { Button } from "../ui/button";
import { Download } from "lucide-react";
import {
Tooltip,
TooltipContent,

View File

@@ -3,18 +3,18 @@ import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import {
type Completion,
type CompletionContext,
type CompletionResult,
autocompletion,
} from "@codemirror/autocomplete";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
import {
autocompletion,
type CompletionContext,
type CompletionResult,
type Completion,
} from "@codemirror/autocomplete";
// Docker Compose completion options
const dockerComposeServices = [

View File

@@ -1,9 +1,9 @@
import type * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type * as React from "react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;

View File

@@ -0,0 +1,31 @@
ALTER TYPE "public"."sourceType" ADD VALUE 'gitea' BEFORE 'drop';--> statement-breakpoint
ALTER TYPE "public"."sourceTypeCompose" ADD VALUE 'gitea' BEFORE 'raw';--> statement-breakpoint
ALTER TYPE "public"."gitProviderType" ADD VALUE 'gitea';--> statement-breakpoint
CREATE TABLE "gitea" (
"giteaId" text PRIMARY KEY NOT NULL,
"giteaUrl" text DEFAULT 'https://gitea.com' NOT NULL,
"redirect_uri" text,
"client_id" text,
"client_secret" text,
"gitProviderId" text NOT NULL,
"gitea_username" text,
"access_token" text,
"refresh_token" text,
"expires_at" integer,
"scopes" text DEFAULT 'repo,repo:status,read:user,read:org',
"last_authenticated_at" integer
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaProjectId" integer;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaBuildPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaRepository" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaOwner" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaBranch" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "giteaId" text;--> statement-breakpoint
ALTER TABLE "gitea" ADD CONSTRAINT "gitea_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "application" ADD CONSTRAINT "application_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compose" ADD CONSTRAINT "compose_giteaId_gitea_giteaId_fk" FOREIGN KEY ("giteaId") REFERENCES "public"."gitea"("giteaId") ON DELETE set null ON UPDATE no action;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" DROP COLUMN "giteaProjectId";

View File

@@ -0,0 +1 @@
ALTER TABLE "gitea" DROP COLUMN "gitea_username";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "backup" ADD COLUMN "userId" text;--> statement-breakpoint
ALTER TABLE "backup" ADD CONSTRAINT "backup_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."databaseType" ADD VALUE 'web-server';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -554,6 +554,41 @@
"when": 1742112194375,
"tag": "0078_uneven_omega_sentinel",
"breakpoints": true
},
{
"idx": 79,
"version": "7",
"when": 1742281690186,
"tag": "0079_bizarre_wendell_rand",
"breakpoints": true
},
{
"idx": 80,
"version": "7",
"when": 1743280866402,
"tag": "0080_sleepy_sinister_six",
"breakpoints": true
},
{
"idx": 81,
"version": "7",
"when": 1743281254393,
"tag": "0081_lovely_mentallo",
"breakpoints": true
},
{
"idx": 82,
"version": "7",
"when": 1743287689974,
"tag": "0082_clean_mandarin",
"breakpoints": true
},
{
"idx": 83,
"version": "7",
"when": 1743288371413,
"tag": "0083_parallel_stranger",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,552 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1713262741218,
"tag": "0000_reflective_puck",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1713761637676,
"tag": "0001_striped_tattoo",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713763492341,
"tag": "0002_ambiguous_carlie_cooper",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713947141424,
"tag": "0003_square_lightspeed",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1714004732716,
"tag": "0004_nice_tenebrous",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1715551130605,
"tag": "0005_cute_terror",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1715563165991,
"tag": "0006_oval_jimmy_woo",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1715563497100,
"tag": "0007_cute_guardsmen",
"breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1715564143641,
"tag": "0008_lazy_sage",
"breakpoints": true
},
{
"idx": 9,
"version": "6",
"when": 1715564774423,
"tag": "0009_majestic_spencer_smythe",
"breakpoints": true
},
{
"idx": 10,
"version": "6",
"when": 1715574037832,
"tag": "0010_lean_black_widow",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1715574230599,
"tag": "0011_petite_calypso",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1716015716708,
"tag": "0012_chubby_umar",
"breakpoints": true
},
{
"idx": 13,
"version": "6",
"when": 1716076179443,
"tag": "0013_blushing_starjammers",
"breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1716715367982,
"tag": "0014_same_hammerhead",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1717564517104,
"tag": "0015_fearless_callisto",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1719109196484,
"tag": "0016_chunky_leopardon",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1719547174326,
"tag": "0017_minor_post",
"breakpoints": true
},
{
"idx": 18,
"version": "6",
"when": 1719928377858,
"tag": "0018_careful_killmonger",
"breakpoints": true
},
{
"idx": 19,
"version": "6",
"when": 1721110706912,
"tag": "0019_heavy_freak",
"breakpoints": true
},
{
"idx": 20,
"version": "6",
"when": 1721363861686,
"tag": "0020_fantastic_slapstick",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1721370423752,
"tag": "0021_premium_sebastian_shaw",
"breakpoints": true
},
{
"idx": 22,
"version": "6",
"when": 1721531163852,
"tag": "0022_warm_colonel_america",
"breakpoints": true
},
{
"idx": 23,
"version": "6",
"when": 1721542782659,
"tag": "0023_icy_maverick",
"breakpoints": true
},
{
"idx": 24,
"version": "6",
"when": 1721603595092,
"tag": "0024_dapper_supernaut",
"breakpoints": true
},
{
"idx": 25,
"version": "6",
"when": 1721633853118,
"tag": "0025_lying_mephisto",
"breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1721979220929,
"tag": "0026_known_dormammu",
"breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1722445099203,
"tag": "0027_red_lady_bullseye",
"breakpoints": true
},
{
"idx": 28,
"version": "6",
"when": 1722503439951,
"tag": "0028_jittery_eternity",
"breakpoints": true
},
{
"idx": 29,
"version": "6",
"when": 1722578386823,
"tag": "0029_colossal_zodiak",
"breakpoints": true
},
{
"idx": 30,
"version": "6",
"when": 1723608499147,
"tag": "0030_little_kabuki",
"breakpoints": true
},
{
"idx": 31,
"version": "6",
"when": 1723701656243,
"tag": "0031_steep_vulture",
"breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1723705257806,
"tag": "0032_flashy_shadow_king",
"breakpoints": true
},
{
"idx": 33,
"version": "6",
"when": 1725250322137,
"tag": "0033_white_hawkeye",
"breakpoints": true
},
{
"idx": 34,
"version": "6",
"when": 1725256397019,
"tag": "0034_aspiring_secret_warriors",
"breakpoints": true
},
{
"idx": 35,
"version": "6",
"when": 1725429324584,
"tag": "0035_cool_gravity",
"breakpoints": true
},
{
"idx": 36,
"version": "6",
"when": 1725519351871,
"tag": "0036_tired_ronan",
"breakpoints": true
},
{
"idx": 37,
"version": "6",
"when": 1726988289562,
"tag": "0037_legal_namor",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1727942090102,
"tag": "0038_rapid_landau",
"breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1728021127765,
"tag": "0039_many_tiger_shark",
"breakpoints": true
},
{
"idx": 40,
"version": "6",
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
},
{
"idx": 41,
"version": "6",
"when": 1729667438853,
"tag": "0041_huge_bruce_banner",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1729984439862,
"tag": "0042_fancy_havok",
"breakpoints": true
},
{
"idx": 43,
"version": "6",
"when": 1731873965888,
"tag": "0043_closed_naoko",
"breakpoints": true
},
{
"idx": 44,
"version": "6",
"when": 1731875539532,
"tag": "0044_sour_true_believers",
"breakpoints": true
},
{
"idx": 45,
"version": "6",
"when": 1732644181718,
"tag": "0045_smiling_blur",
"breakpoints": true
},
{
"idx": 46,
"version": "6",
"when": 1732851191048,
"tag": "0046_purple_sleeper",
"breakpoints": true
},
{
"idx": 47,
"version": "6",
"when": 1733599090582,
"tag": "0047_tidy_revanche",
"breakpoints": true
},
{
"idx": 48,
"version": "6",
"when": 1733599163710,
"tag": "0048_flat_expediter",
"breakpoints": true
},
{
"idx": 49,
"version": "6",
"when": 1733628762978,
"tag": "0049_dark_leopardon",
"breakpoints": true
},
{
"idx": 50,
"version": "6",
"when": 1733889104203,
"tag": "0050_nappy_wrecker",
"breakpoints": true
},
{
"idx": 51,
"version": "6",
"when": 1734241482851,
"tag": "0051_hard_gorgon",
"breakpoints": true
},
{
"idx": 52,
"version": "6",
"when": 1734809337308,
"tag": "0052_bumpy_luckman",
"breakpoints": true
},
{
"idx": 53,
"version": "6",
"when": 1735118844878,
"tag": "0053_broken_kulan_gath",
"breakpoints": true
},
{
"idx": 54,
"version": "6",
"when": 1736669421560,
"tag": "0054_nervous_spencer_smythe",
"breakpoints": true
},
{
"idx": 55,
"version": "6",
"when": 1736669623831,
"tag": "0055_next_serpent_society",
"breakpoints": true
},
{
"idx": 56,
"version": "6",
"when": 1736789918294,
"tag": "0056_majestic_skaar",
"breakpoints": true
},
{
"idx": 57,
"version": "6",
"when": 1737306063563,
"tag": "0057_tricky_living_tribunal",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737612903012,
"tag": "0058_brown_sharon_carter",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737615160768,
"tag": "0059_striped_bill_hollister",
"breakpoints": true
},
{
"idx": 60,
"version": "6",
"when": 1737929896838,
"tag": "0060_disable-aggressive-cache",
"breakpoints": true
},
{
"idx": 61,
"version": "7",
"when": 1738481304953,
"tag": "0061_many_molten_man",
"breakpoints": true
},
{
"idx": 62,
"version": "7",
"when": 1738482795112,
"tag": "0062_slippery_white_tiger",
"breakpoints": true
},
{
"idx": 63,
"version": "7",
"when": 1738522845992,
"tag": "0063_panoramic_dreadnoughts",
"breakpoints": true
},
{
"idx": 64,
"version": "7",
"when": 1738564387043,
"tag": "0064_previous_agent_brand",
"breakpoints": true
},
{
"idx": 65,
"version": "7",
"when": 1739087857244,
"tag": "0065_daily_zaladane",
"breakpoints": true
},
{
"idx": 66,
"version": "7",
"when": 1739426913392,
"tag": "0066_yielding_echo",
"breakpoints": true
},
{
"idx": 67,
"version": "7",
"when": 1740892043121,
"tag": "0067_condemned_sugar_man",
"breakpoints": true
},
{
"idx": 68,
"version": "7",
"when": 1740897756774,
"tag": "0068_complex_rhino",
"breakpoints": true
},
{
"idx": 69,
"version": "7",
"when": 1741152916611,
"tag": "0069_legal_bill_hollister",
"breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1741322697251,
"tag": "0070_useful_serpent_society",
"breakpoints": true
},
{
"idx": 71,
"version": "7",
"when": 1741559743256,
"tag": "0071_flimsy_plazm",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1741593124105,
"tag": "0072_low_redwing",
"breakpoints": true
},
{
"idx": 73,
"version": "7",
"when": 1741645208694,
"tag": "0073_dark_tigra",
"breakpoints": true
},
{
"idx": 74,
"version": "7",
"when": 1741673569715,
"tag": "0074_military_miss_america",
"breakpoints": true
},
{
"idx": 75,
"version": "7",
"when": 1742018928109,
"tag": "0075_wild_xorn",
"breakpoints": true
},
{
"idx": 76,
"version": "7",
"when": 1742237840762,
"tag": "0076_tough_iron_patriot",
"breakpoints": true
},
{
"idx": 77,
"version": "7",
"when": 1742238314349,
"tag": "0077_mature_tomorrow_man",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.20.8",
"version": "v0.21.1",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -150,7 +150,8 @@
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"zod-form-data": "^2.0.2"
"zod-form-data": "^2.0.2",
"toml": "3.0.0"
},
"devDependencies": {
"@types/adm-zip": "^0.5.5",

View File

@@ -84,6 +84,33 @@ export default async function handler(
res.status(301).json({ message: "Branch Not Match" });
return;
}
const provider = getProviderByHeader(req.headers);
let normalizedCommits: string[] = [];
if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
}
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
@@ -128,6 +155,27 @@ export default async function handler(
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== application.giteaBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {
@@ -280,6 +328,26 @@ export const extractBranchName = (headers: any, body: any) => {
return null;
};
export const getProviderByHeader = (headers: any) => {
if (headers["x-github-event"]) {
return "github";
}
if (headers["x-gitea-event"]) {
return "gitea";
}
if (headers["x-gitlab-event"]) {
return "gitlab";
}
if (headers["x-event-key"]?.includes("repo:push")) {
return "bitbucket";
}
return null;
};
export const extractCommitedPaths = async (
body: any,
bitbucketUsername: string | null,

View File

@@ -8,9 +8,10 @@ import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import {
extractBranchName,
extractCommitedPaths,
extractCommitMessage,
extractCommitedPaths,
extractHash,
getProviderByHeader,
} from "../[refreshToken]";
export default async function handler(
@@ -91,12 +92,6 @@ export default async function handler(
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const commitedPaths = await extractCommitedPaths(
req.body,
@@ -104,6 +99,7 @@ export default async function handler(
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
commitedPaths,
@@ -113,6 +109,59 @@ export default async function handler(
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const provider = getProviderByHeader(req.headers);
let normalizedCommits: string[] = [];
if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
} else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
}
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
} else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== composeResult.giteaBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {

View File

@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { findGitea, redirectWithError } from "./helper";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
if (req.method !== "GET") {
return res.status(405).json({ error: "Method not allowed" });
}
const { giteaId } = req.query;
if (!giteaId || Array.isArray(giteaId)) {
return res.status(400).json({ error: "Invalid Gitea provider ID" });
}
const gitea = await findGitea(giteaId as string);
if (!gitea || !gitea.clientId || !gitea.redirectUri) {
return redirectWithError(res, "Incomplete OAuth configuration");
}
// Generate the Gitea authorization URL
const authorizationUrl = new URL(`${gitea.giteaUrl}/login/oauth/authorize`);
authorizationUrl.searchParams.append("client_id", gitea.clientId as string);
authorizationUrl.searchParams.append("response_type", "code");
authorizationUrl.searchParams.append(
"redirect_uri",
gitea.redirectUri as string,
);
authorizationUrl.searchParams.append("scope", "read:user repo");
authorizationUrl.searchParams.append("state", giteaId as string);
// Redirect user to Gitea authorization URL
return res.redirect(307, authorizationUrl.toString());
} catch (error) {
console.error("Error initiating Gitea OAuth flow:", error);
return res.status(500).json({ error: "Internal server error" });
}
}

View File

@@ -0,0 +1,93 @@
import { updateGitea } from "@dokploy/server";
import type { NextApiRequest, NextApiResponse } from "next";
import { type Gitea, findGitea, redirectWithError } from "./helper";
// Helper to parse the state parameter
const parseState = (state: string): string | null => {
try {
const stateObj =
state.startsWith("{") && state.endsWith("}") ? JSON.parse(state) : {};
return stateObj.giteaId || state || null;
} catch {
return null;
}
};
// Helper to fetch access token from Gitea
const fetchAccessToken = async (gitea: Gitea, code: string) => {
const response = await fetch(`${gitea.giteaUrl}/login/oauth/access_token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: gitea.clientId as string,
client_secret: gitea.clientSecret as string,
code,
grant_type: "authorization_code",
redirect_uri: gitea.redirectUri || "",
}),
});
const responseText = await response.text();
return response.ok
? JSON.parse(responseText)
: { error: "Token exchange failed", responseText };
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, state } = req.query;
if (!code || Array.isArray(code) || !state || Array.isArray(state)) {
return redirectWithError(
res,
"Invalid authorization code or state parameter",
);
}
const giteaId = parseState(state as string);
if (!giteaId) return redirectWithError(res, "Invalid state format");
const gitea = await findGitea(giteaId);
if (!gitea) return redirectWithError(res, "Failed to find Gitea provider");
// Fetch the access token from Gitea
const result = await fetchAccessToken(gitea, code as string);
if (result.error) {
console.error("Token exchange failed:", result);
return redirectWithError(res, result.error);
}
if (!result.access_token) {
console.error("Missing access token:", result);
return redirectWithError(res, "No access token received");
}
const expiresAt = result.expires_in
? Math.floor(Date.now() / 1000) + result.expires_in
: null;
try {
await updateGitea(gitea.giteaId, {
accessToken: result.access_token,
refreshToken: result.refresh_token,
expiresAt,
...(result.organizationName
? { organizationName: result.organizationName }
: {}),
});
return res.redirect(
307,
"/dashboard/settings/git-providers?connected=true",
);
} catch (updateError) {
console.error("Failed to update Gitea provider:", updateError);
return redirectWithError(res, "Failed to store access token");
}
}

View File

@@ -0,0 +1,39 @@
import { findGiteaById } from "@dokploy/server";
import type { NextApiResponse } from "next";
export interface Gitea {
giteaId: string;
gitProviderId: string;
redirectUri: string | null;
accessToken: string | null;
refreshToken: string | null;
expiresAt: number | null;
giteaUrl: string;
clientId: string | null;
clientSecret: string | null;
organizationName?: string;
gitProvider: {
name: string;
gitProviderId: string;
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
createdAt: string;
organizationId: string;
};
}
export const findGitea = async (giteaId: string): Promise<Gitea | null> => {
try {
const gitea = await findGiteaById(giteaId);
return gitea;
} catch (findError) {
console.error("Error finding Gitea provider:", findError);
return null;
}
};
export const redirectWithError = (res: NextApiResponse, error: string) => {
return res.redirect(
307,
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
);
};

View File

@@ -34,6 +34,15 @@ import {
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -47,6 +56,13 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
@@ -64,8 +80,8 @@ import {
Loader2,
PlusIcon,
Search,
X,
Trash2,
X,
} from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -73,25 +89,10 @@ import type {
} from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { type ReactElement, useMemo, useState, useEffect } from "react";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = {
appName: string;
@@ -553,7 +554,7 @@ const Project = (
</CardTitle>
<CardDescription>{data?.description}</CardDescription>
</CardHeader>
{(auth?.role === "owner" || auth?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap justify-between items-center">
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button>
@@ -569,7 +570,7 @@ const Project = (
className="w-[200px] space-y-2"
align="end"
>
<DropdownMenuLabel className="text-sm font-normal ">
<DropdownMenuLabel className="text-sm font-normal">
Actions
</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -593,7 +594,7 @@ const Project = (
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
{isLoading ? (
@@ -670,20 +671,27 @@ const Project = (
</DialogAction>
{(auth?.role === "owner" ||
auth?.canDeleteServices) && (
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
type="destructive"
onClick={handleBulkDelete}
>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
<>
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
type="destructive"
onClick={handleBulkDelete}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</DialogAction>
<Button
variant="ghost"
className="w-full justify-start text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
</DialogAction>
<DuplicateProject
projectId={projectId}
services={applications}
selectedServiceIds={selectedServices}
/>
</>
)}
<Dialog

View File

@@ -1,3 +1,4 @@
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
@@ -47,7 +48,6 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
type TabState =
| "projects"

View File

@@ -8,11 +8,11 @@ import { ShowExternalPostgresCredentials } from "@/components/dashboard/postgres
import { ShowGeneralPostgres } from "@/components/dashboard/postgres/general/show-general-postgres";
import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres/general/show-internal-postgres-credentials";
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { Badge } from "@/components/ui/badge";
import {
Card,

View File

@@ -5,7 +5,7 @@ import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const Page = () => {

View File

@@ -8,56 +8,21 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { Card } from "@/components/ui/card";
const Page = () => {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<WebDomain />
<WebServer />
{/* <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
Paid Features
</CardTitle>
<CardDescription>
Enable or disable paid features like monitoring
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-row gap-2 items-center">
<span className="text-sm font-medium text-muted-foreground">
Enable Paid Features:
</span>
<Switch
checked={data?.enablePaidFeatures}
onCheckedChange={() => {
update({
enablePaidFeatures: !data?.enablePaidFeatures,
})
.then(() => {
toast.success(
`Paid features ${
data?.enablePaidFeatures ? "disabled" : "enabled"
} successfully`,
);
refetch();
})
.catch(() => {
toast.error("Error updating paid features");
});
}}
/>
</div>
</CardContent>
{data?.enablePaidFeatures && <SetupMonitoring />}
</div>
</Card> */}
{/* */}
<div className="w-full flex flex-col gap-4">
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<ShowBackups id={user?.userId ?? ""} type="web-server" />
</Card>
</div>
</div>
</div>
);

View File

@@ -12,6 +12,7 @@ import { destinationRouter } from "./routers/destination";
import { dockerRouter } from "./routers/docker";
import { domainRouter } from "./routers/domain";
import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { mariadbRouter } from "./routers/mariadb";
@@ -68,6 +69,7 @@ export const appRouter = createTRPCRouter({
notification: notificationRouter,
sshKey: sshRouter,
gitProvider: gitProviderRouter,
gitea: giteaRouter,
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
github: githubRouter,

View File

@@ -26,8 +26,8 @@ import {
checkServiceAccess,
} from "@dokploy/server/services/user";
import {
getProviderHeaders,
type Model,
getProviderHeaders,
} from "@dokploy/server/utils/ai/select-ai-provider";
import { TRPCError } from "@trpc/server";
import { z } from "zod";

View File

@@ -14,6 +14,7 @@ import {
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGiteaProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
@@ -399,6 +400,31 @@ export const applicationRouter = createTRPCRouter({
watchPaths: input.watchPaths,
});
return true;
}),
saveGiteaProvider: protectedProcedure
.input(apiSaveGiteaProvider)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this gitea provider",
});
}
await updateApplication(input.applicationId, {
giteaRepository: input.giteaRepository,
giteaOwner: input.giteaOwner,
giteaBranch: input.giteaBranch,
giteaBuildPath: input.giteaBuildPath,
sourceType: "gitea",
applicationStatus: "idle",
giteaId: input.giteaId,
watchPaths: input.watchPaths,
});
return true;
}),
saveDockerProvider: protectedProcedure

View File

@@ -25,25 +25,27 @@ import {
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
runWebServerBackup,
scheduleBackup,
updateBackupById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findDestinationById } from "@dokploy/server/services/destination";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
import { findDestinationById } from "@dokploy/server/services/destination";
import {
restoreMariadbBackup,
restoreMongoBackup,
restoreMySqlBackup,
restorePostgresBackup,
restoreWebServerBackup,
} from "@dokploy/server/utils/restore";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
export const backupRouter = createTRPCRouter({
create: protectedProcedure
@@ -85,9 +87,13 @@ export const backupRouter = createTRPCRouter({
}
}
} catch (error) {
console.error(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the Backup",
message:
error instanceof Error
? error.message
: "Error creating the Backup",
cause: error,
});
}
@@ -227,6 +233,13 @@ export const backupRouter = createTRPCRouter({
});
}
}),
manualBackupWebServer: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
const backup = await findBackupById(input.backupId);
await runWebServerBackup(backup);
return true;
}),
listBackupFiles: protectedProcedure
.input(
z.object({
@@ -301,7 +314,13 @@ export const backupRouter = createTRPCRouter({
.input(
z.object({
databaseId: z.string(),
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
databaseType: z.enum([
"postgres",
"mysql",
"mariadb",
"mongo",
"web-server",
]),
databaseName: z.string().min(1),
backupFile: z.string().min(1),
destinationId: z.string().min(1),
@@ -366,6 +385,13 @@ export const backupRouter = createTRPCRouter({
);
});
}
if (input.databaseType === "web-server") {
return observable<string>((emit) => {
restoreWebServerBackup(destination, input.backupFile, (log) => {
emit.next(log);
});
});
}
return true;
}),

View File

@@ -9,23 +9,10 @@ import {
apiUpdateCompose,
compose as composeTable,
} from "@/server/db/schema";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { generatePassword } from "@/templates/utils";
import {
type CompleteTemplate,
fetchTemplateFiles,
fetchTemplatesList,
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump, load } from "js-yaml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { z } from "zod";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import {
IS_CLOUD,
addDomainToCompose,
@@ -55,6 +42,20 @@ import {
stopCompose,
updateCompose,
} from "@dokploy/server";
import {
type CompleteTemplate,
fetchTemplateFiles,
fetchTemplatesList,
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { parse } from "toml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
@@ -594,7 +595,7 @@ export const composeRouter = createTRPCRouter({
serverIp = "127.0.0.1";
}
const templateData = JSON.parse(decodedData);
const config = load(templateData.config) as CompleteTemplate;
const config = parse(templateData.config) as CompleteTemplate;
if (!templateData.compose || !config) {
throw new TRPCError({
@@ -663,7 +664,8 @@ export const composeRouter = createTRPCRouter({
}
const templateData = JSON.parse(decodedData);
const config = load(templateData.config) as CompleteTemplate;
const config = parse(templateData.config) as CompleteTemplate;
if (!templateData.compose || !config) {
throw new TRPCError({
@@ -678,7 +680,6 @@ export const composeRouter = createTRPCRouter({
projectName: compose.appName,
});
// Update compose file
await updateCompose(input.composeId, {
composeFile: templateData.compose,
sourceType: "raw",
@@ -686,7 +687,6 @@ export const composeRouter = createTRPCRouter({
isolatedDeployment: true,
});
// Create mounts
if (processedTemplate.mounts && processedTemplate.mounts.length > 0) {
for (const mount of processedTemplate.mounts) {
await createMount({
@@ -700,7 +700,6 @@ export const composeRouter = createTRPCRouter({
}
}
// Create domains
if (processedTemplate.domains && processedTemplate.domains.length > 0) {
for (const domain of processedTemplate.domains) {
await createDomain({

View File

@@ -21,7 +21,7 @@ import {
updateDestinationById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { eq, desc } from "drizzle-orm";
import { desc, eq } from "drizzle-orm";
export const destinationRouter = createTRPCRouter({
create: adminProcedure

View File

@@ -12,6 +12,7 @@ export const gitProviderRouter = createTRPCRouter({
gitlab: true,
bitbucket: true,
github: true,
gitea: true,
},
orderBy: desc(gitProvider.createdAt),
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),

View File

@@ -0,0 +1,245 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateGitea,
apiFindGiteaBranches,
apiFindOneGitea,
apiGiteaTestConnection,
apiUpdateGitea,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
createGitea,
findGiteaById,
getGiteaBranches,
getGiteaRepositories,
haveGiteaRequirements,
testGiteaConnection,
updateGitProvider,
updateGitea,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const giteaRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating this Gitea provider",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
return giteaProvider;
}),
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
let result = await db.query.gitea.findMany({
with: {
gitProvider: true,
},
});
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
const filtered = result
.filter((provider) => haveGiteaRequirements(provider))
.map((provider) => {
return {
giteaId: provider.giteaId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const { giteaId } = input;
if (!giteaId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID is required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
const repositories = await getGiteaRepositories(giteaId);
return repositories;
} catch (error) {
console.error("Error fetching Gitea repositories:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches)
.query(async ({ input, ctx }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Gitea provider ID, owner, and repository name are required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
return await getGiteaBranches({
giteaId,
owner,
repo: repositoryName,
});
} catch (error) {
console.error("Error fetching Gitea branches:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(async ({ input, ctx }) => {
const giteaId = input.giteaId ?? "";
try {
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
const result = await testGiteaConnection({
giteaId,
});
return `Found ${result} repositories`;
} catch (error) {
console.error("Gitea connection test error:", error);
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error.message : String(error),
});
}
}),
update: protectedProcedure
.input(apiUpdateGitea)
.mutation(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
organizationId: ctx.session.activeOrganizationId,
});
await updateGitea(input.giteaId, {
...input,
});
} else {
await updateGitea(input.giteaId, {
...input,
});
}
return { success: true };
}),
getGiteaUrl: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const { giteaId } = input;
if (!giteaId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Gitea provider ID is required.",
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
// Return the base URL of the Gitea instance
return giteaProvider.giteaUrl;
}),
});

View File

@@ -1,14 +1,15 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiChangeMariaDBStatus,
apiCreateMariaDB,
apiDeployMariaDB,
apiFindOneMariaDB,
apiRebuildMariadb,
apiResetMariadb,
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
apiRebuildMariadb,
mariadb as mariadbTable,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
@@ -30,12 +31,11 @@ import {
stopServiceRemote,
updateMariadbById,
} from "@dokploy/server";
import { rebuildDatabase } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { rebuildDatabase } from "@dokploy/server";
import { z } from "zod";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMariaDB)

View File

@@ -1,4 +1,5 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiChangeMongoStatus,
apiCreateMongo,
@@ -30,12 +31,11 @@ import {
stopServiceRemote,
updateMongoById,
} from "@dokploy/server";
import { rebuildDatabase } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { rebuildDatabase } from "@dokploy/server";
import { z } from "zod";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMongo)

View File

@@ -14,6 +14,7 @@ import {
import { TRPCError } from "@trpc/server";
import { db } from "@/server/db";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
@@ -36,7 +37,6 @@ import {
} from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const mysqlRouter = createTRPCRouter({

View File

@@ -1,4 +1,5 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiChangePostgresStatus,
apiCreatePostgres,
@@ -33,9 +34,8 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
export const postgresRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePostgres)

View File

@@ -14,21 +14,44 @@ import {
projects,
redis,
} from "@/server/db/schema";
import { z } from "zod";
import {
IS_CLOUD,
addNewProject,
checkProjectAccess,
createApplication,
createCompose,
createMariadb,
createMongo,
createMysql,
createPostgres,
createProject,
createRedis,
deleteProject,
findApplicationById,
findComposeById,
findMongoById,
findMemberById,
findRedisById,
findProjectById,
findUserById,
updateProjectById,
findPostgresById,
findMariadbById,
findMySqlById,
createDomain,
createPort,
createMount,
createRedirect,
createPreviewDeployment,
createBackup,
createSecurity,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
export const projectRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateProject)
@@ -266,7 +289,317 @@ export const projectRouter = createTRPCRouter({
throw error;
}
}),
duplicate: protectedProcedure
.input(
z.object({
sourceProjectId: z.string(),
name: z.string(),
description: z.string().optional(),
includeServices: z.boolean().default(true),
selectedServices: z
.array(
z.object({
id: z.string(),
type: z.enum([
"application",
"postgres",
"mariadb",
"mongo",
"mysql",
"redis",
"compose",
]),
}),
)
.optional(),
}),
)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.rol === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
// Get source project
const sourceProject = await findProjectById(input.sourceProjectId);
if (sourceProject.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
// Create new project
const newProject = await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
// Helper function to duplicate a service
const duplicateService = async (id: string, type: string) => {
switch (type) {
case "application": {
const {
applicationId,
domains,
security,
ports,
registry,
redirects,
previewDeployments,
mounts,
...application
} = await findApplicationById(id);
const newApplication = await createApplication({
...application,
projectId: newProject.projectId,
});
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
applicationId: newApplication.applicationId,
domainType: "application",
});
}
for (const port of ports) {
const { portId, ...rest } = port;
await createPort({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newApplication.applicationId,
serviceType: "application",
});
}
for (const redirect of redirects) {
const { redirectId, ...rest } = redirect;
await createRedirect({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const secure of security) {
const { securityId, ...rest } = secure;
await createSecurity({
...rest,
applicationId: newApplication.applicationId,
});
}
for (const previewDeployment of previewDeployments) {
const { previewDeploymentId, ...rest } = previewDeployment;
await createPreviewDeployment({
...rest,
applicationId: newApplication.applicationId,
});
}
break;
}
case "postgres": {
const { postgresId, mounts, backups, ...postgres } =
await findPostgresById(id);
const newPostgres = await createPostgres({
...postgres,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
postgresId: newPostgres.postgresId,
});
}
break;
}
case "mariadb": {
const { mariadbId, mounts, backups, ...mariadb } =
await findMariadbById(id);
const newMariadb = await createMariadb({
...mariadb,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMariadb.mariadbId,
serviceType: "mariadb",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
mariadbId: newMariadb.mariadbId,
});
}
break;
}
case "mongo": {
const { mongoId, mounts, backups, ...mongo } =
await findMongoById(id);
const newMongo = await createMongo({
...mongo,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMongo.mongoId,
serviceType: "mongo",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
mongoId: newMongo.mongoId,
});
}
break;
}
case "mysql": {
const { mysqlId, mounts, backups, ...mysql } =
await findMySqlById(id);
const newMysql = await createMysql({
...mysql,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newMysql.mysqlId,
serviceType: "mysql",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
mysqlId: newMysql.mysqlId,
});
}
break;
}
case "redis": {
const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({
...redis,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newRedis.redisId,
serviceType: "redis",
});
}
break;
}
case "compose": {
const { composeId, mounts, domains, ...compose } =
await findComposeById(id);
const newCompose = await createCompose({
...compose,
projectId: newProject.projectId,
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
}
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (ctx.user.rol === "member") {
await addNewProject(
ctx.user.id,
newProject.projectId,
ctx.session.activeOrganizationId,
);
}
return newProject;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error duplicating the project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
});
function buildServiceFilter(
fieldName: AnyPgColumn,
accessedServices: string[],

View File

@@ -4,16 +4,17 @@ import {
apiCreateRedis,
apiDeployRedis,
apiFindOneRedis,
apiRebuildRedis,
apiResetRedis,
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
redis as redisTable,
apiRebuildRedis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { db } from "@/server/db";
import {
IS_CLOUD,
addNewService,
@@ -31,11 +32,10 @@ import {
stopServiceRemote,
updateRedisById,
} from "@dokploy/server";
import { rebuildDatabase } from "@dokploy/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { db } from "@/server/db";
import { z } from "zod";
import { rebuildDatabase } from "@dokploy/server";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)

View File

@@ -26,6 +26,7 @@ import {
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
initializeTraefik,
parseRawConfig,
@@ -41,6 +42,8 @@ import {
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
@@ -48,9 +51,6 @@ import {
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
@@ -479,6 +479,7 @@ export const settingsRouter = createTRPCRouter({
"bitbucket",
"github",
"gitlab",
"gitea",
],
});

View File

@@ -1,11 +1,11 @@
import {
IS_CLOUD,
createApiKey,
findOrganizationById,
findUserById,
getUserByToken,
removeUserById,
updateUser,
createApiKey,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
@@ -13,12 +13,12 @@ import {
apiAssignPermissions,
apiFindOneToken,
apiUpdateUser,
apikey,
invitation,
member,
apikey,
} from "@dokploy/server/db/schema";
import * as bcrypt from "bcrypt";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, asc, eq, gt } from "drizzle-orm";
import { z } from "zod";
import {
@@ -91,6 +91,28 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: {
with: {
backups: {
with: {
destination: true,
},
},
apiKeys: true,
},
},
},
});
return memberResult?.user;
}),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(

View File

@@ -46,7 +46,7 @@ void app.prepare().then(async () => {
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
initCronJobs();
await initCronJobs();
await migration();
await sendDokployRestartNotifications();
}

View File

@@ -0,0 +1,87 @@
// utils/gitea-utils.ts
// This file contains client-safe utilities for Gitea integration
/**
* Generates an OAuth URL for Gitea authorization
*
* @param giteaId The ID of the Gitea provider to be used as state
* @param clientId The OAuth client ID
* @param giteaUrl The base URL of the Gitea instance
* @param baseUrl The base URL of the application for callback
* @returns The complete OAuth authorization URL
*/
export const getGiteaOAuthUrl = (
giteaId: string,
clientId: string,
giteaUrl: string,
baseUrl: string,
): string => {
if (!clientId || !giteaUrl || !baseUrl) {
// Return a marker that can be checked by the caller
return "#";
}
const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
const scopes = "repo repo:status read:user read:org";
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirectUri,
)}&response_type=code&scope=${encodeURIComponent(scopes)}&state=${encodeURIComponent(giteaId)}`;
};
// Interfaces for Gitea API responses and components
export interface Repository {
name: string;
url: string;
id: number;
owner: {
username: string;
};
}
export interface Branch {
name: string;
}
export interface GiteaProviderType {
giteaId: string;
gitProvider: {
name: string;
gitProviderId: string;
providerType: "github" | "gitlab" | "bitbucket" | "gitea";
createdAt: string;
organizationId: string;
};
name: string;
}
export interface GiteaProviderResponse {
giteaId: string;
clientId: string;
giteaUrl: string;
}
export interface GitProvider {
gitProviderId: string;
name: string;
providerType: string;
giteaId?: string;
gitea?: {
giteaId: string;
giteaUrl: string;
clientId: string;
};
}
export interface GiteaProvider {
gitea?: {
giteaId?: string;
giteaUrl?: string;
clientId?: string;
clientSecret?: string;
redirectUri?: string;
organizationName?: string;
};
name?: string;
gitProviderId?: string;
}

View File

@@ -28,7 +28,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"micromatch":"4.0.8",
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -36,11 +36,11 @@
"@ai-sdk/mistral": "^1.0.6",
"@ai-sdk/openai": "^1.0.12",
"@ai-sdk/openai-compatible": "^0.0.13",
"@better-auth/utils":"0.2.3",
"@oslojs/encoding":"1.1.0",
"@oslojs/crypto":"1.0.1",
"drizzle-dbml-generator":"0.10.0",
"better-auth":"1.2.4",
"@better-auth/utils": "0.2.3",
"@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.4",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",
@@ -53,7 +53,7 @@
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-orm": "^0.39.1",
"drizzle-orm": "^0.39.1",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
@@ -76,7 +76,8 @@
"ws": "8.16.0",
"zod": "^3.23.4",
"ssh2": "1.15.0",
"@octokit/rest": "^20.0.2"
"@octokit/rest": "^20.0.2",
"toml": "3.0.0"
},
"devDependencies": {
"@types/micromatch": "4.0.9",

View File

@@ -13,6 +13,7 @@ import { z } from "zod";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
@@ -33,6 +34,7 @@ export const sourceType = pgEnum("sourceType", [
"github",
"gitlab",
"bitbucket",
"gitea",
"drop",
]);
@@ -155,6 +157,11 @@ export const applications = pgTable("application", {
gitlabBranch: text("gitlabBranch"),
gitlabBuildPath: text("gitlabBuildPath").default("/"),
gitlabPathNamespace: text("gitlabPathNamespace"),
// Gitea
giteaRepository: text("giteaRepository"),
giteaOwner: text("giteaOwner"),
giteaBranch: text("giteaBranch"),
giteaBuildPath: text("giteaBuildPath").default("/"),
// Bitbucket
bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"),
@@ -212,6 +219,9 @@ export const applications = pgTable("application", {
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
onDelete: "set null",
}),
giteaId: text("giteaId").references(() => gitea.giteaId, {
onDelete: "set null",
}),
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null",
}),
@@ -249,6 +259,10 @@ export const applicationsRelations = relations(
fields: [applications.gitlabId],
references: [gitlab.gitlabId],
}),
gitea: one(gitea, {
fields: [applications.giteaId],
references: [gitea.giteaId],
}),
bitbucket: one(bitbucket, {
fields: [applications.bitbucketId],
references: [bitbucket.bitbucketId],
@@ -379,7 +393,9 @@ const createSchema = createInsertSchema(applications, {
customGitUrl: z.string().optional(),
buildPath: z.string().optional(),
projectId: z.string(),
sourceType: z.enum(["github", "docker", "git"]).optional(),
sourceType: z
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
.optional(),
applicationStatus: z.enum(["idle", "running", "done", "error"]),
buildType: z.enum([
"dockerfile",
@@ -483,6 +499,18 @@ export const apiSaveBitbucketProvider = createSchema
})
.required();
export const apiSaveGiteaProvider = createSchema
.pick({
applicationId: true,
giteaBranch: true,
giteaBuildPath: true,
giteaOwner: true,
giteaRepository: true,
giteaId: true,
watchPaths: true,
})
.required();
export const apiSaveDockerProvider = createSchema
.pick({
dockerImage: true,

View File

@@ -15,12 +15,13 @@ import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { users_temp } from "./user";
export const databaseType = pgEnum("databaseType", [
"postgres",
"mariadb",
"mysql",
"mongo",
"web-server",
]);
export const backups = pgTable("backup", {
@@ -58,6 +59,7 @@ export const backups = pgTable("backup", {
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => users_temp.id),
});
export const backupsRelations = relations(backups, ({ one }) => ({
@@ -81,6 +83,10 @@ export const backupsRelations = relations(backups, ({ one }) => ({
fields: [backups.mongoId],
references: [mongo.mongoId],
}),
user: one(users_temp, {
fields: [backups.userId],
references: [users_temp.id],
}),
}));
const createSchema = createInsertSchema(backups, {
@@ -91,11 +97,12 @@ const createSchema = createInsertSchema(backups, {
database: z.string().min(1),
schedule: z.string(),
keepLatestCount: z.number().optional(),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
postgresId: z.string().optional(),
mariadbId: z.string().optional(),
mysqlId: z.string().optional(),
mongoId: z.string().optional(),
userId: z.string().optional(),
});
export const apiCreateBackup = createSchema.pick({
@@ -110,6 +117,7 @@ export const apiCreateBackup = createSchema.pick({
postgresId: true,
mongoId: true,
databaseType: true,
userId: true,
});
export const apiFindOneBackup = createSchema

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { mounts } from "./mount";
@@ -20,6 +21,7 @@ export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"github",
"gitlab",
"bitbucket",
"gitea",
"raw",
]);
@@ -55,6 +57,10 @@ export const compose = pgTable("compose", {
bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"),
// Gitea
giteaRepository: text("giteaRepository"),
giteaOwner: text("giteaOwner"),
giteaBranch: text("giteaBranch"),
// Git
customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"),
@@ -87,6 +93,9 @@ export const compose = pgTable("compose", {
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null",
}),
giteaId: text("giteaId").references(() => gitea.giteaId, {
onDelete: "set null",
}),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
@@ -116,6 +125,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
fields: [compose.bitbucketId],
references: [bitbucket.bitbucketId],
}),
gitea: one(gitea, {
fields: [compose.giteaId],
references: [gitea.giteaId],
}),
server: one(server, {
fields: [compose.serverId],
references: [server.serverId],
@@ -126,7 +139,7 @@ const createSchema = createInsertSchema(compose, {
name: z.string().min(1),
description: z.string(),
env: z.string().optional(),
composeFile: z.string().min(1),
composeFile: z.string().optional(),
projectId: z.string(),
customGitSSHKeyId: z.string().optional(),
command: z.string().optional(),
@@ -142,6 +155,7 @@ export const apiCreateCompose = createSchema.pick({
composeType: true,
appName: true,
serverId: true,
composeFile: true,
});
export const apiCreateComposeByTemplate = createSchema

View File

@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { bitbucket } from "./bitbucket";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -12,6 +13,7 @@ export const gitProviderType = pgEnum("gitProviderType", [
"github",
"gitlab",
"bitbucket",
"gitea",
]);
export const gitProvider = pgTable("git_provider", {
@@ -42,6 +44,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
fields: [gitProvider.gitProviderId],
references: [bitbucket.gitProviderId],
}),
gitea: one(gitea, {
fields: [gitProvider.gitProviderId],
references: [gitea.gitProviderId],
}),
organization: one(organization, {
fields: [gitProvider.organizationId],
references: [organization.id],

View File

@@ -0,0 +1,86 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
export const gitea = pgTable("gitea", {
giteaId: text("giteaId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
redirectUri: text("redirect_uri"),
clientId: text("client_id"),
clientSecret: text("client_secret"),
gitProviderId: text("gitProviderId")
.notNull()
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
expiresAt: integer("expires_at"),
scopes: text("scopes").default("repo,repo:status,read:user,read:org"),
lastAuthenticatedAt: integer("last_authenticated_at"),
});
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
gitProvider: one(gitProvider, {
fields: [gitea.gitProviderId],
references: [gitProvider.gitProviderId],
}),
}));
const createSchema = createInsertSchema(gitea);
export const apiCreateGitea = createSchema.extend({
clientId: z.string().optional(),
clientSecret: z.string().optional(),
gitProviderId: z.string().optional(),
redirectUri: z.string().optional(),
name: z.string().min(1),
giteaUrl: z.string().min(1),
giteaUsername: z.string().optional(),
accessToken: z.string().optional(),
refreshToken: z.string().optional(),
expiresAt: z.number().optional(),
organizationName: z.string().optional(),
scopes: z.string().optional(),
lastAuthenticatedAt: z.number().optional(),
});
export const apiFindOneGitea = createSchema
.extend({
giteaId: z.string().min(1),
})
.pick({ giteaId: true });
export const apiGiteaTestConnection = createSchema
.extend({
organizationName: z.string().optional(),
})
.pick({ giteaId: true, organizationName: true });
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
export const apiFindGiteaBranches = z.object({
owner: z.string().min(1),
repositoryName: z.string().min(1),
giteaId: z.string().optional(),
});
export const apiUpdateGitea = createSchema.extend({
clientId: z.string().optional(),
clientSecret: z.string().optional(),
redirectUri: z.string().optional(),
name: z.string().min(1),
giteaId: z.string().min(1),
giteaUrl: z.string().min(1),
giteaUsername: z.string().optional(),
accessToken: z.string().optional(),
refreshToken: z.string().optional(),
expiresAt: z.number().optional(),
organizationName: z.string().optional(),
scopes: z.string().optional(),
lastAuthenticatedAt: z.number().optional(),
});

View File

@@ -25,6 +25,7 @@ export * from "./git-provider";
export * from "./bitbucket";
export * from "./github";
export * from "./gitlab";
export * from "./gitea";
export * from "./server";
export * from "./utils";
export * from "./preview-deployments";

View File

@@ -52,6 +52,7 @@ const createSchema = createInsertSchema(projects, {
export const apiCreateProject = createSchema.pick({
name: true,
description: true,
env: true,
});
export const apiFindOneProject = createSchema

View File

@@ -46,6 +46,7 @@ enum gitProviderType {
github
gitlab
bitbucket
gitea
}
enum mountType {
@@ -98,6 +99,7 @@ enum sourceType {
github
gitlab
bitbucket
gitea
drop
}
@@ -106,6 +108,7 @@ enum sourceTypeCompose {
github
gitlab
bitbucket
gitea
raw
}
@@ -206,6 +209,7 @@ table application {
githubId text
gitlabId text
bitbucketId text
giteaId text
serverId text
}
@@ -280,6 +284,9 @@ table compose {
bitbucketRepository text
bitbucketOwner text
bitbucketBranch text
giteaRepository text
giteaOwner text
giteaBranch text
customGitUrl text
customGitBranch text
customGitSSHKeyId text
@@ -294,6 +301,7 @@ table compose {
githubId text
gitlabId text
bitbucketId text
giteaId text
serverId text
}
@@ -389,6 +397,20 @@ table gitlab {
gitProviderId text [not null]
}
table gitea {
giteaId text [pk, not null]
giteaUrl text [not null, default: 'https://gitea.com']
redirect_uri text
client_id text [not null]
client_secret text [not null]
access_token text
refresh_token text
expires_at integer
gitProviderId text [not null]
scopes text [default: 'repo,repo:status,read:user,read:org']
last_authenticated_at integer
}
table gotify {
gotifyId text [pk, not null]
serverUrl text [not null]
@@ -820,6 +842,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
ref: gitlab.gitProviderId - git_provider.gitProviderId
ref: gitea.gitProviderId - git_provider.gitProviderId
ref: git_provider.userId - user.id
ref: mariadb.projectId > project.projectId

View File

@@ -10,9 +10,10 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { account, organization, apikey } from "./account";
import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -124,6 +125,7 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
organizations: many(organization),
projects: many(projects),
apiKeys: many(apikey),
backups: many(backups),
}));
const createSchema = createInsertSchema(users_temp, {

View File

@@ -28,6 +28,7 @@ export * from "./services/git-provider";
export * from "./services/bitbucket";
export * from "./services/github";
export * from "./services/gitlab";
export * from "./services/gitea";
export * from "./services/server";
export * from "./services/application";
export * from "./utils/databases/rebuild";
@@ -47,6 +48,7 @@ export * from "./utils/backups/mongo";
export * from "./utils/backups/mysql";
export * from "./utils/backups/postgres";
export * from "./utils/backups/utils";
export * from "./utils/backups/web-server";
export * from "./templates/processors";
export * from "./utils/notifications/build-error";
@@ -90,6 +92,7 @@ export * from "./utils/providers/docker";
export * from "./utils/providers/git";
export * from "./utils/providers/github";
export * from "./utils/providers/gitlab";
export * from "./utils/providers/gitea";
export * from "./utils/providers/raw";
export * from "./utils/servers/remote-docker";

View File

@@ -2,16 +2,16 @@ import type { IncomingMessage } from "node:http";
import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization, twoFactor, apiKey } from "better-auth/plugins";
import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { db } from "../db";
import * as schema from "../db/schema";
import { sendEmail } from "../verification/send-verification-email";
import { IS_CLOUD } from "../constants";
import { getPublicIpWithFallback } from "../wss/utils";
import { updateUser } from "../services/user";
import { getUserByToken } from "../services/admin";
import { APIError } from "better-auth/api";
import { updateUser } from "../services/user";
import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
const { handler, api } = betterAuth({
database: drizzleAdapter(db, {

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findServerById } from "./server";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({

Some files were not shown because too many files have changed in this diff Show More