mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01d92d1d9 | ||
|
|
8f0a4f0886 | ||
|
|
01794c7742 | ||
|
|
19bbe69ee3 | ||
|
|
92c4e769ab | ||
|
|
0801e91816 | ||
|
|
b360cc2af4 | ||
|
|
ba5d8feba2 | ||
|
|
43be9d0171 | ||
|
|
a6bbf5d96b | ||
|
|
7e6e43adfe | ||
|
|
e8a4611ab7 | ||
|
|
608db3d401 | ||
|
|
0add62f14d | ||
|
|
1754f63352 | ||
|
|
edcc7544fb | ||
|
|
1edf30546d | ||
|
|
ad806437af | ||
|
|
f0eecf354b | ||
|
|
c5ace67b3f | ||
|
|
fe22890311 | ||
|
|
3f2eeaf386 | ||
|
|
7826ba5bb5 | ||
|
|
bdc488e179 | ||
|
|
fc2abac989 | ||
|
|
101bbd44d8 | ||
|
|
68c2272e98 | ||
|
|
bc28464430 | ||
|
|
7df415a386 | ||
|
|
9fbd3039a6 | ||
|
|
3c00937b94 | ||
|
|
ae4226531e | ||
|
|
a57a776500 | ||
|
|
323e2f54ba | ||
|
|
2b7c7632f4 | ||
|
|
7db662e332 | ||
|
|
cd1a686b59 | ||
|
|
78d573c4f3 | ||
|
|
0ef9b1427b | ||
|
|
993d6b52f2 | ||
|
|
b9ab4a4d1a | ||
|
|
83153471b8 | ||
|
|
909e536f45 | ||
|
|
295cf50060 | ||
|
|
dd16baf234 | ||
|
|
72c366aa10 | ||
|
|
9a4f79f9e6 | ||
|
|
30b81834fc | ||
|
|
4e3aaa2a69 | ||
|
|
3dcd89cc32 | ||
|
|
41dc388bb0 | ||
|
|
44a592f7a7 | ||
|
|
1a4f5607dc | ||
|
|
d54c6e4ac9 | ||
|
|
936cf76a4c | ||
|
|
65254f1686 | ||
|
|
b312b1d7e0 | ||
|
|
1b7244e841 | ||
|
|
7eb1716d71 | ||
|
|
b8a8e32659 | ||
|
|
eea00d28cd | ||
|
|
449a61e208 | ||
|
|
037f3ed118 | ||
|
|
471802c4da | ||
|
|
502ff638d6 | ||
|
|
750c12ded5 | ||
|
|
7fb34ade00 | ||
|
|
0c197c095b | ||
|
|
4cbff731d1 | ||
|
|
b9bff95c3d | ||
|
|
113df9ae12 | ||
|
|
7f13fd24ec | ||
|
|
290228116a | ||
|
|
fae180f157 |
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before opening a new issue, please do a search of existing issues.
|
||||
|
||||
If you need help with your own project, you can start a discussion in the [Q&A Section](https://github.com/Dokploy/dokploy/discussions).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: A step-by-step description of how to reproduce the issue, or a link to the reproducible repository.
|
||||
placeholder: |
|
||||
1. Create a application
|
||||
2. Click X
|
||||
3. Y will happen
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current vs. Expected behavior
|
||||
description: A clear and concise description of what the bug is, and what you expected to happen.
|
||||
placeholder: 'Following the steps from the previous section, I expected A to happen, but I observed B instead'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: Please provide the following information about your environment.
|
||||
render: bash
|
||||
placeholder: |
|
||||
Operating System:
|
||||
OS: Ubuntu 20.04
|
||||
Arch: arm64
|
||||
Dokploy version: 0.2.2'
|
||||
VPS Provider: DigitalOcean, Hetzner, Linode, etc.
|
||||
What applications/services are you tying to deploy?
|
||||
eg - Database, Nextjs App, laravel, etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Installation'
|
||||
- 'Application'
|
||||
- 'Databases'
|
||||
- 'Docker Compose'
|
||||
- 'Traefik'
|
||||
- 'Docker'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Any extra information that might help us investigate.
|
||||
placeholder: |
|
||||
I tested on a DigitalOcean VPS with Ubuntu 20.04 and Docker version 20.10.12.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions?
|
||||
url: https://github.com/Dokploy/dokploy/discussions
|
||||
about: Ask your questions here.
|
||||
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement to the project
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What problem will this feature address?
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: |
|
||||
I'm always frustrated when I can't do X
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: Add X to the core
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: |
|
||||
Maybe use Y as a workaround?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ yarn-error.log*
|
||||
|
||||
*.lockb
|
||||
*.rdb
|
||||
.idea
|
||||
|
||||
@@ -39,10 +39,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
Getestete Systems:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Dokumentation
|
||||
|
||||
|
||||
@@ -40,10 +40,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
Проверенные системы:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 Документация
|
||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
|
||||
@@ -43,10 +43,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
||||
|
||||
经过测试的系统:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 24.04 LTS (Noble Numbat)
|
||||
- Ubuntu 23.10 (Mantic Minotaur)
|
||||
- Ubuntu 22.04 LTS (Jammy Jellyfish)
|
||||
- Ubuntu 20.04 LTS (Focal Fossa)
|
||||
- Ubuntu 18.04 LTS (Bionic Beaver)
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
## 📄 文档
|
||||
|
||||
|
||||
91
README.md
91
README.md
@@ -1,36 +1,37 @@
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center" style="width:100%;">
|
||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<br />
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases using Docker and Traefik. Designed to enhance efficiency and security, Dokploy allows you to deploy your applications on any VPS.
|
||||
Dokploy include multiples features to make your life easier.
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
* **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||
* **Backups**: Automate backups for databases to a external storage destination.
|
||||
* **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
* **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
||||
* **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
||||
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
||||
* **Docker Management**: Easily deploy and manage Docker containers.
|
||||
* **CLI (Soon⌛)**: Manage your applications and databases using the command line.
|
||||
* **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
|
||||
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis, and more.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage.
|
||||
- **Database Backups**: Automate backups with support for multiple storage destinations.
|
||||
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
To get started run the following command in a VPS:
|
||||
@@ -40,14 +41,54 @@ To get started run the following command in a VPS:
|
||||
curl -sSL https://dokploy.com/install.sh | sh
|
||||
```
|
||||
|
||||
Tested Systems:
|
||||
|
||||
- Ubuntu 20.04
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com).
|
||||
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
|
||||
## Video Tutorial
|
||||
<a href="https://youtu.be/mznYKPvhcfw">
|
||||
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||
</a>
|
||||
|
||||
|
||||
## Donations
|
||||
|
||||
If you like dokploy, and want to support the project to cover the costs of hosting, testing and development new features, you can donate to the project using the following link:
|
||||
|
||||
Thanks to all the supporters!
|
||||
|
||||
https://opencollective.com/dokploy
|
||||
|
||||
|
||||
<a href="https://opencollective.com/dokploy"><img src="https://opencollective.com/dokploy/individuals.svg?width=890"></a>
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
## Support OS
|
||||
|
||||
- Ubuntu 24.04 LTS
|
||||
- Ubuntu 23.10
|
||||
- Ubuntu 22.04 LTS
|
||||
- Ubuntu 20.04 LTS
|
||||
- Ubuntu 18.04 LTS
|
||||
- Debian 12
|
||||
- Debian 11
|
||||
- Fedora 40
|
||||
- Centos 9
|
||||
- Centos 8
|
||||
|
||||
|
||||
|
||||
## Explanation
|
||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
||||
|
||||
|
||||
|
||||
@@ -183,8 +183,7 @@ export const AddPort = ({
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent defaultValue={"none"}>
|
||||
<SelectItem value={"none"}>None</SelectItem>
|
||||
<SelectContent>
|
||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -58,6 +58,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
||||
};
|
||||
|
||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
@@ -81,7 +82,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||
@@ -100,6 +101,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
refetch();
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update the traefik config");
|
||||
@@ -107,7 +110,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
if (!open) {
|
||||
form.reset();
|
||||
}
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button isLoading={isLoading}>Modify</Button>
|
||||
</DialogTrigger>
|
||||
@@ -122,7 +130,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<form
|
||||
id="hook-form-update-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 overflow-auto"
|
||||
className="w-full space-y-4 overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
|
||||
@@ -189,7 +189,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="bind"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Bind Mount
|
||||
</Label>
|
||||
@@ -209,7 +209,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="volume"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Volume Mount
|
||||
</Label>
|
||||
@@ -233,7 +233,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="file"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
File Mount
|
||||
</Label>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@@ -33,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } =
|
||||
api.application.saveEnvironment.useMutation();
|
||||
|
||||
@@ -72,22 +74,57 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
toast.error("Error to add environment");
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
@@ -111,7 +149,12 @@ PORT=3000
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
@@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application Deploying....");
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Application Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
|
||||
import { StartApplication } from "../start-application";
|
||||
import { StopApplication } from "../stop-application";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { CheckCircle2, Terminal } from "lucide-react";
|
||||
import { DeployApplication } from "./deploy-application";
|
||||
import { ResetApplication } from "./reset-application";
|
||||
interface Props {
|
||||
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
toast.error("Error to update Auto Deploy");
|
||||
});
|
||||
}}
|
||||
className="flex flex-row gap-2 items-center"
|
||||
>
|
||||
Autodeploy
|
||||
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||
</Toggle>
|
||||
<RedbuildApplication applicationId={applicationId} />
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@@ -32,6 +34,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
@@ -71,21 +74,57 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEnvVisible) {
|
||||
if (data?.env) {
|
||||
const maskedLines = data.env
|
||||
.split("\n")
|
||||
.map((line) => "*".repeat(line.length))
|
||||
.join("\n");
|
||||
form.reset({
|
||||
environment: maskedLines,
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
environment: "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
form.reset({
|
||||
environment: data?.env || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form, isEnvVisible]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||
<CardDescription>
|
||||
You can add environment variables to your resource.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle bold"
|
||||
pressed={isEnvVisible}
|
||||
onPressedChange={setIsEnvVisible}
|
||||
>
|
||||
{isEnvVisible ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Toggle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
@@ -109,7 +149,12 @@ PORT=3000
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button isLoading={isLoading} className="w-fit" type="submit">
|
||||
<Button
|
||||
disabled={isEnvVisible}
|
||||
isLoading={isLoading}
|
||||
className="w-fit"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full relative gap-4"
|
||||
className="w-full relative space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -99,8 +99,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
<CodeEditor
|
||||
// disabled
|
||||
value={field.value}
|
||||
className="font-mono min-h-[20rem] compose-file-editor"
|
||||
wrapperClassName="min-h-[20rem]"
|
||||
className="font-mono"
|
||||
wrapperClassName="compose-file-editor"
|
||||
placeholder={`version: '3'
|
||||
services:
|
||||
web:
|
||||
|
||||
@@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => {
|
||||
composeStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Deploying....");
|
||||
toast.success("Deploying Compose....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Compose");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full relative z-[5]"
|
||||
className="w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<FormField
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mariadbId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Root Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databaseRootPassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||
/>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mongoId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||
/>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
mysqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
@@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Root Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databaseRootPassword}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||
/>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database....");
|
||||
await refetch();
|
||||
await deploy({
|
||||
postgresId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
@@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
disabled
|
||||
<ToggleVisibilityInput
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||
/>
|
||||
|
||||
@@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Folder } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
const AddTemplateSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddApplication = ({ projectId }: Props) => {
|
||||
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.application.create.useMutation();
|
||||
@@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
const form = useForm<AddTemplate>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
appName: `${slug}-`,
|
||||
description: "",
|
||||
},
|
||||
resolver: zodResolver(AddTemplateSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
|
||||
const onSubmit = async (data: AddTemplate) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
description: data.description,
|
||||
projectId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Created");
|
||||
form.reset();
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
toast.error("Error to create the service");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
@@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Frontend" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Frontend"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
@@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="buildType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Build Type</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={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="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>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -34,12 +34,22 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
const AddComposeSchema = z.object({
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddCompose = ({ projectId }: Props) => {
|
||||
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const slug = slugify(projectName);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
|
||||
@@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
name: "",
|
||||
description: "",
|
||||
composeType: "docker-compose",
|
||||
appName: `${slug}-`,
|
||||
},
|
||||
resolver: zodResolver(AddComposeSchema),
|
||||
});
|
||||
@@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
description: data.description,
|
||||
projectId,
|
||||
composeType: data.composeType,
|
||||
appName: data.appName,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
@@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Frontend" {...field} />
|
||||
<Input
|
||||
placeholder="Frontend"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composeType"
|
||||
|
||||
@@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Database } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Database, AlertTriangle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
|
||||
|
||||
const baseDatabaseSchema = z.object({
|
||||
name: z.string().min(1, "Name required"),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: "App name is required",
|
||||
})
|
||||
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
|
||||
message:
|
||||
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
|
||||
}),
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string(),
|
||||
description: z.string().nullable(),
|
||||
@@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
|
||||
.merge(baseDatabaseSchema),
|
||||
]);
|
||||
|
||||
const databasesMap = {
|
||||
postgres: {
|
||||
icon: <PostgresqlIcon />,
|
||||
label: "PostgreSQL",
|
||||
},
|
||||
mongo: {
|
||||
icon: <MongodbIcon />,
|
||||
label: "MongoDB",
|
||||
},
|
||||
mariadb: {
|
||||
icon: <MariadbIcon />,
|
||||
label: "MariaDB",
|
||||
},
|
||||
mysql: {
|
||||
icon: <MysqlIcon />,
|
||||
label: "MySQL",
|
||||
},
|
||||
redis: {
|
||||
icon: <RedisIcon />,
|
||||
label: "Redis",
|
||||
},
|
||||
};
|
||||
|
||||
type AddDatabase = z.infer<typeof mySchema>;
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddDatabase = ({ projectId }: Props) => {
|
||||
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
|
||||
|
||||
const { mutateAsync: createRedis } = api.redis.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
|
||||
|
||||
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
const mysqlMutation = api.mysql.create.useMutation();
|
||||
|
||||
const form = useForm<AddDatabase>({
|
||||
defaultValues: {
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
appName: `${slug}-`,
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
@@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
const type = form.watch("type");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
databaseUser: "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
const activeMutation = {
|
||||
postgres: postgresMutation,
|
||||
mongo: mongoMutation,
|
||||
redis: redisMutation,
|
||||
mariadb: mariadbMutation,
|
||||
mysql: mysqlMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: AddDatabase) => {
|
||||
const defaultDockerImage =
|
||||
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
||||
|
||||
let promise: Promise<unknown> | null = null;
|
||||
const commonParams = {
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
dockerImage: defaultDockerImage,
|
||||
projectId,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
if (data.type === "postgres") {
|
||||
promise = createPostgresql({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = postgresMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mongo") {
|
||||
promise = createMongo({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mongoMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "redis") {
|
||||
promise = createRedis({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = redisMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
projectId,
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mariadb") {
|
||||
promise = createMariadb({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mariadbMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
projectId,
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
description: data.description,
|
||||
});
|
||||
} else if (data.type === "mysql") {
|
||||
promise = createMysql({
|
||||
name: data.name,
|
||||
dockerImage: defaultDockerImage,
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName,
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
projectId,
|
||||
databaseRootPassword: data.databaseRootPassword,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
await promise
|
||||
.then(async () => {
|
||||
toast.success("Database Created");
|
||||
form.reset({
|
||||
type: "postgres",
|
||||
dockerImage: "",
|
||||
name: "",
|
||||
appName: `${projectName}-`,
|
||||
databasePassword: "",
|
||||
description: "",
|
||||
databaseName: "",
|
||||
databaseUser: "",
|
||||
});
|
||||
setVisible(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
@@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
@@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
<span>Database</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Databases</DialogTitle>
|
||||
</DialogHeader>
|
||||
{/* {isError && (
|
||||
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
defaultValue={field.value}
|
||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<FormItem className="flex w-full items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="postgres"
|
||||
id="postgres"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="postgres"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<PostgresqlIcon />
|
||||
Postgresql
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mysql"
|
||||
id="mysql"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mysql"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MysqlIcon />
|
||||
Mysql
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mariadb"
|
||||
id="mariadb"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mariadb"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MariadbIcon />
|
||||
Mariadb
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="mongo"
|
||||
id="mongo"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="mongo"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<MongodbIcon />
|
||||
Mongo
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value="redis"
|
||||
id="redis"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="redis"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<RedisIcon />
|
||||
Redis
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
{Object.entries(databasesMap).map(([key, value]) => (
|
||||
<FormItem
|
||||
key={key}
|
||||
className="flex w-full items-center space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl className="w-full">
|
||||
<div>
|
||||
<RadioGroupItem
|
||||
value={key}
|
||||
id={key}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
{value.icon}
|
||||
{value.label}
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{activeMutation[field.value].isError && (
|
||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{activeMutation[field.value].error?.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} />
|
||||
<Input
|
||||
placeholder="Name"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value?.trim() || "";
|
||||
form.setValue("appName", `${slug}-${val}`);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AppName</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 "
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -50,17 +50,13 @@ export const DeployRedis = ({ redisId }: Props) => {
|
||||
applicationStatus: "running",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Database Deploying....");
|
||||
toast.success("Deploying Database...");
|
||||
await refetch();
|
||||
await deploy({
|
||||
redisId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Database Deployed Succesfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Database");
|
||||
});
|
||||
await refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<Input disabled value={connectionUrl} />
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
|
||||
interface Props {
|
||||
redisId: string;
|
||||
@@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input
|
||||
disabled
|
||||
<ToggleVisibilityInput
|
||||
value={data?.databasePassword}
|
||||
type="password"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<Input
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
||||
/>
|
||||
|
||||
73
components/dashboard/settings/profile/generate-token.tsx
Normal file
73
components/dashboard/settings/profile/generate-token.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Link from "next/link";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
|
||||
export const GenerateToken = () => {
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
|
||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||
api.auth.generateToken.useMutation();
|
||||
|
||||
return (
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||
<div>
|
||||
<CardTitle className="text-xl">API/CLI</CardTitle>
|
||||
<CardDescription>
|
||||
Generate a token to access the API/CLI
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 max-sm:flex-wrap items-end">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Swagger API:
|
||||
</span>
|
||||
<Link
|
||||
href="/swagger"
|
||||
target="_blank"
|
||||
className="flex flex-row gap-2 items-center"
|
||||
>
|
||||
<span className="text-sm font-medium">View</span>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex flex-row gap-2 max-sm:flex-wrap justify-end items-end">
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Token</Label>
|
||||
<ToggleVisibilityInput
|
||||
placeholder="Token"
|
||||
value={data?.token || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
isLoading={isLoadingToken}
|
||||
onClick={async () => {
|
||||
await generateToken().then(() => {
|
||||
refetch();
|
||||
toast.success("Token generated");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -51,6 +51,9 @@ const randomImages = [
|
||||
export const ProfileForm = () => {
|
||||
const { data, refetch } = api.auth.get.useQuery();
|
||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||
|
||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||
api.auth.generateToken.useMutation();
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
email: data?.email || "",
|
||||
|
||||
@@ -32,7 +32,6 @@ export const ShowSettings = () => {
|
||||
<ShowCertificates />
|
||||
<WebDomain />
|
||||
<WebServer />
|
||||
|
||||
<ShowUsers />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -38,6 +38,7 @@ const addPermissions = z.object({
|
||||
canDeleteServices: z.boolean().optional().default(false),
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
@@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
@@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
accesedProjects: data.accesedProjects || [],
|
||||
accesedServices: data.accesedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
@@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToAPI"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to API/CLI</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the API/CLI
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accesedProjects"
|
||||
|
||||
@@ -194,7 +194,7 @@ export const WebServer = () => {
|
||||
Space
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start">
|
||||
<DropdownMenuContent className="w-64" align="start">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<form
|
||||
id="hook-form-update-main-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative"
|
||||
className="w-full space-y-4 relative"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative overflow-auto"
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
|
||||
@@ -98,7 +98,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
<form
|
||||
id="hook-form-update-server-traefik-config"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full py-4 relative overflow-auto"
|
||||
className="w-full space-y-4 relative overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<FormField
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CodeEditor = ({
|
||||
)}
|
||||
/>
|
||||
{props.disabled && (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
|
||||
<div className="absolute top-0 rounded-md left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
26
components/shared/toggle-visibility-input.tsx
Normal file
26
components/shared/toggle-visibility-input.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useState } from "react";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { Input, type InputProps } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
setIsPasswordVisible((prevVisibility) => !prevVisibility);
|
||||
};
|
||||
|
||||
const inputType = isPasswordVisible ? "text" : "password";
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Input type={inputType} {...props} />
|
||||
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
|
||||
{inputType === "password" ? (
|
||||
<EyeIcon className="size-4 text-muted-foreground" />
|
||||
) : (
|
||||
<EyeOffIcon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -36,12 +36,14 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<div className="space-y-4 w-full">
|
||||
{children}
|
||||
</div>
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
|
||||
1
drizzle/0015_fearless_callisto.sql
Normal file
1
drizzle/0015_fearless_callisto.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "auth" ADD COLUMN "token" text;
|
||||
1
drizzle/0016_chunky_leopardon.sql
Normal file
1
drizzle/0016_chunky_leopardon.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "canAccessToAPI" boolean DEFAULT false NOT NULL;
|
||||
1
drizzle/0017_yummy_norrin_radd.sql
Normal file
1
drizzle/0017_yummy_norrin_radd.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;
|
||||
2613
drizzle/meta/0015_snapshot.json
Normal file
2613
drizzle/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2620
drizzle/meta/0016_snapshot.json
Normal file
2620
drizzle/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2620
drizzle/meta/0017_snapshot.json
Normal file
2620
drizzle/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,27 @@
|
||||
"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": 1719109531147,
|
||||
"tag": "0017_yummy_norrin_radd",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
15
lib/slug.ts
Normal file
15
lib/slug.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import slug from "slugify";
|
||||
|
||||
export const slugify = (text: string | undefined) => {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||
|
||||
return slug(cleanedText, {
|
||||
lower: true,
|
||||
trim: true,
|
||||
strict: true,
|
||||
});
|
||||
};
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.2.0",
|
||||
"version": "v0.2.4",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
@@ -31,11 +31,11 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/language":"^6.10.1",
|
||||
"@aws-sdk/client-s3": "3.515.0",
|
||||
"@codemirror/legacy-modes":"6.4.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||
@@ -76,6 +76,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
@@ -105,17 +106,18 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"recharts": "^2.12.3",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.4.0",
|
||||
"superjson": "^2.2.1",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tar-fs": "3.0.5",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"use-resize-observer": "9.1.0",
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"copy-webpack-plugin": "^12.0.2"
|
||||
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.7.1",
|
||||
@@ -129,6 +131,7 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/tar-fs": "2.0.4",
|
||||
"@types/ws": "8.5.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
|
||||
@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
|
||||
|
||||
// export API handler
|
||||
export default createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
onError:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
onError:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@@ -210,9 +210,12 @@ const Project = (
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<AddApplication projectId={projectId} />
|
||||
<AddDatabase projectId={projectId} />
|
||||
<AddCompose projectId={projectId} />
|
||||
<AddApplication
|
||||
projectId={projectId}
|
||||
projectName={data?.name}
|
||||
/>
|
||||
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||
<AddTemplate projectId={projectId} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<ProfileForm />
|
||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
63
pages/swagger.tsx
Normal file
63
pages/swagger.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { api } from "@/utils/api";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext, NextPage } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
import superjson from "superjson";
|
||||
|
||||
const SwaggerUI = dynamic(() => import("swagger-ui-react"), { ssr: false });
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { data } = api.settings.getOpenApiDocument.useQuery();
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white">
|
||||
<SwaggerUI spec={data || {}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
const { user, session } = await validateRequest(context.req, context.res);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
if (user.rol === "user") {
|
||||
const result = await helpers.user.byAuthId.fetch({
|
||||
authId: user.id,
|
||||
});
|
||||
|
||||
if (!result.canAccessToAPI) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
1605
pnpm-lock.yaml
generated
1605
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ import { dockerRouter } from "./routers/docker";
|
||||
import { composeRouter } from "./routers/compose";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { clusterRouter } from "./routers/cluster";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@@ -39,6 +40,7 @@ export const appRouter = createTRPCRouter({
|
||||
redis: redisRouter,
|
||||
mongo: mongoRouter,
|
||||
mariadb: mariadbRouter,
|
||||
compose: composeRouter,
|
||||
user: userRouter,
|
||||
domain: domainRouter,
|
||||
destination: destinationRouter,
|
||||
@@ -50,7 +52,6 @@ export const appRouter = createTRPCRouter({
|
||||
security: securityRouter,
|
||||
redirects: redirectsRouter,
|
||||
port: portRouter,
|
||||
compose: composeRouter,
|
||||
registry: registryRouter,
|
||||
cluster: clusterRouter,
|
||||
});
|
||||
|
||||
@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
|
||||
if (ctx.user.rol === "user") {
|
||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the application",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
updateAuthById,
|
||||
verify2FA,
|
||||
} from "../services/auth";
|
||||
import { luciaToken } from "@/server/auth/token";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
createAdmin: publicProcedure
|
||||
@@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
|
||||
return auth;
|
||||
}),
|
||||
|
||||
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||
const auth = await findAuthById(ctx.user.authId);
|
||||
|
||||
if (auth.token) {
|
||||
await luciaToken.invalidateSession(auth.token);
|
||||
}
|
||||
const session = await luciaToken.createSession(auth?.id || "", {
|
||||
expiresIn: 60 * 60 * 24 * 30,
|
||||
});
|
||||
|
||||
await updateAuthById(auth.id, {
|
||||
token: session.id,
|
||||
});
|
||||
|
||||
return auth;
|
||||
}),
|
||||
|
||||
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
||||
const auth = await findAuthById(input.id);
|
||||
return auth;
|
||||
@@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
|
||||
});
|
||||
return auth;
|
||||
}),
|
||||
verifyToken: protectedProcedure.mutation(async () => {
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
|
||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { createCommand } from "@/server/utils/builders/compose";
|
||||
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
|
||||
import {
|
||||
generatePassword,
|
||||
loadTemplateModule,
|
||||
readComposeFile,
|
||||
} from "@/templates/utils";
|
||||
import { findAdmin } from "../services/admin";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
||||
import { findProjectById } from "../services/project";
|
||||
import { createMount } from "../services/mount";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
import { templates } from "@/templates/templates";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
const project = await findProjectById(input.projectId);
|
||||
|
||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
||||
const projectName = slugify(`${project.name} ${input.id}`);
|
||||
const { envs, mounts } = generate({
|
||||
serverIp: admin.serverIp,
|
||||
projectName: projectName,
|
||||
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
||||
env: envs.join("\n"),
|
||||
name: input.id,
|
||||
sourceType: "raw",
|
||||
appName: `${projectName}-${generatePassword(6)}`,
|
||||
});
|
||||
|
||||
if (ctx.user.rol === "user") {
|
||||
|
||||
@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mariadb database",
|
||||
|
||||
@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mongo database",
|
||||
|
||||
@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mysql database",
|
||||
|
||||
@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting postgresql database",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
cliProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateProject,
|
||||
@@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { docker } from "@/server/constants";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export const registryRouter = createTRPCRouter({
|
||||
create: adminProcedure
|
||||
@@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
|
||||
.input(apiTestRegistry)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const result = await docker.checkAuth({
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
serveraddress: input.registryUrl,
|
||||
});
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log("Error Registry:", error);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -41,6 +41,8 @@ import {
|
||||
} from "../services/settings";
|
||||
import { canAccessToTraefikFiles } from "../services/user";
|
||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { appRouter } from "../root";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
@@ -242,5 +244,50 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
return readConfigInPath(input.path);
|
||||
}),
|
||||
|
||||
getOpenApiDocument: protectedProcedure.query(
|
||||
async ({ ctx }): Promise<unknown> => {
|
||||
const protocol = ctx.req.headers["x-forwarded-proto"];
|
||||
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`;
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: "tRPC OpenAPI",
|
||||
version: "1.0.0",
|
||||
baseUrl: url,
|
||||
docsUrl: `${url}/settings.getOpenApiDocument`,
|
||||
tags: [
|
||||
"admin",
|
||||
"docker",
|
||||
"compose",
|
||||
"registry",
|
||||
"cluster",
|
||||
"user",
|
||||
"domain",
|
||||
"destination",
|
||||
"backup",
|
||||
"deployment",
|
||||
"mounts",
|
||||
"certificates",
|
||||
"settings",
|
||||
"security",
|
||||
"redirects",
|
||||
"port",
|
||||
"project",
|
||||
"application",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
"mongo",
|
||||
"mariadb",
|
||||
],
|
||||
});
|
||||
|
||||
openApiDocument.info = {
|
||||
title: "Dokploy API",
|
||||
description: "Endpoints for dokploy",
|
||||
version: getDokployVersion(),
|
||||
};
|
||||
|
||||
return openApiDocument;
|
||||
},
|
||||
),
|
||||
});
|
||||
// apt-get install apache2-utils
|
||||
|
||||
@@ -15,11 +15,27 @@ import { findAdmin } from "./admin";
|
||||
import { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||
import { docker } from "@/server/constants";
|
||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const newApplication = await tx
|
||||
.insert(applications)
|
||||
|
||||
@@ -13,10 +13,25 @@ import { join } from "node:path";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
@@ -39,6 +54,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
export const createComposeByTemplate = async (
|
||||
input: typeof compose.$inferInsert,
|
||||
) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
|
||||
@@ -46,9 +46,7 @@ export const getContainers = async () => {
|
||||
.filter((container) => !container.name.includes("dokploy"));
|
||||
|
||||
return containers;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getConfig = async (containerId: string) => {
|
||||
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {
|
||||
console.error(`Execution error: ${error}`);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
@@ -5,10 +5,26 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMariadb = await db
|
||||
.insert(mariadb)
|
||||
.values({
|
||||
|
||||
@@ -5,10 +5,26 @@ import { buildMongo } from "@/server/utils/databases/mongo";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMongo = await db
|
||||
.insert(mongo)
|
||||
.values({
|
||||
|
||||
@@ -5,11 +5,27 @@ import { buildMysql } from "@/server/utils/databases/mysql";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
|
||||
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newMysql = await db
|
||||
.insert(mysql)
|
||||
.values({
|
||||
|
||||
@@ -5,10 +5,26 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPostgres = await db
|
||||
.insert(postgres)
|
||||
.values({
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { db } from "@/server/db";
|
||||
import { type apiCreateProject, projects } from "@/server/db/schema";
|
||||
import {
|
||||
type apiCreateProject,
|
||||
applications,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
projects,
|
||||
redis,
|
||||
} from "@/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { findAdmin } from "./admin";
|
||||
@@ -75,12 +84,40 @@ export const updateProjectById = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const slugifyProjectName = (projectName: string): string => {
|
||||
return projectName
|
||||
.toLowerCase()
|
||||
.replace(/[0-9]/g, "")
|
||||
.replace(/[^a-z\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
export const validUniqueServerAppName = async (appName: string) => {
|
||||
const query = await db.query.projects.findMany({
|
||||
with: {
|
||||
applications: {
|
||||
where: eq(applications.appName, appName),
|
||||
},
|
||||
mariadb: {
|
||||
where: eq(mariadb.appName, appName),
|
||||
},
|
||||
mongo: {
|
||||
where: eq(mongo.appName, appName),
|
||||
},
|
||||
mysql: {
|
||||
where: eq(mysql.appName, appName),
|
||||
},
|
||||
postgres: {
|
||||
where: eq(postgres.appName, appName),
|
||||
},
|
||||
redis: {
|
||||
where: eq(redis.appName, appName),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Filter out items with non-empty fields
|
||||
const nonEmptyProjects = query.filter(
|
||||
(project) =>
|
||||
project.applications.length > 0 ||
|
||||
project.mariadb.length > 0 ||
|
||||
project.mongo.length > 0 ||
|
||||
project.mysql.length > 0 ||
|
||||
project.postgres.length > 0 ||
|
||||
project.redis.length > 0,
|
||||
);
|
||||
|
||||
return nonEmptyProjects.length === 0;
|
||||
};
|
||||
|
||||
@@ -5,11 +5,27 @@ import { buildRedis } from "@/server/utils/databases/redis";
|
||||
import { pullImage } from "@/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { generateAppName } from "@/server/db/schema/utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newRedis = await db
|
||||
.insert(redis)
|
||||
.values({
|
||||
|
||||
@@ -9,28 +9,35 @@ import {
|
||||
} from "@/server/utils/traefik/registry";
|
||||
import { removeService } from "@/server/utils/docker/utils";
|
||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
|
||||
export type Registry = typeof registry.$inferSelect;
|
||||
|
||||
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||
const admin = await findAdmin();
|
||||
|
||||
const newRegistry = await db
|
||||
.insert(registry)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
return await db.transaction(async (tx) => {
|
||||
const newRegistry = await tx
|
||||
.insert(registry)
|
||||
.values({
|
||||
...input,
|
||||
adminId: admin.adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newRegistry) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
return newRegistry;
|
||||
if (!newRegistry) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting registry",
|
||||
});
|
||||
}
|
||||
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
await execAsync(loginCommand);
|
||||
|
||||
return newRegistry;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeRegistry = async (registryId: string) => {
|
||||
@@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
|
||||
await removeService("dokploy-registry");
|
||||
}
|
||||
|
||||
await execAsync(`docker logout ${response.registryUrl}`);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -15,6 +15,8 @@ import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { validateRequest } from "../auth/auth";
|
||||
import type { Session, User } from "lucia";
|
||||
import { validateBearerToken } from "../auth/token";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
@@ -59,9 +61,15 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
*/
|
||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
const { req, res } = opts;
|
||||
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||
const { session, user } = await validateRequest(req, res);
|
||||
user;
|
||||
|
||||
let { session, user } = await validateBearerToken(req);
|
||||
|
||||
if (!session) {
|
||||
const cookieResult = await validateRequest(req, res);
|
||||
session = cookieResult.session;
|
||||
user = cookieResult.user;
|
||||
}
|
||||
|
||||
return createInnerTRPCContext({
|
||||
req,
|
||||
res,
|
||||
@@ -88,19 +96,22 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
* errors on the backend.
|
||||
*/
|
||||
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError:
|
||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const t = initTRPC
|
||||
.meta<OpenApiMeta>()
|
||||
.context<typeof createTRPCContext>()
|
||||
.create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError:
|
||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
@@ -147,6 +158,20 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: ctx.session,
|
||||
user: ctx.user,
|
||||
// session: { ...ctx.session, user: ctx.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
@@ -33,14 +33,17 @@ declare module "lucia" {
|
||||
}
|
||||
}
|
||||
|
||||
export type ReturnValidateToken = Promise<{
|
||||
user: (User & { authId: string }) | null;
|
||||
session: Session | null;
|
||||
}>;
|
||||
|
||||
export async function validateRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<{
|
||||
user: (User & { authId: string }) | null;
|
||||
session: Session | null;
|
||||
}> {
|
||||
): ReturnValidateToken {
|
||||
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||
|
||||
if (!sessionId) {
|
||||
return {
|
||||
user: null,
|
||||
|
||||
48
server/auth/token.ts
Normal file
48
server/auth/token.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Lucia } from "lucia/dist/core.js";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { TimeSpan } from "lucia";
|
||||
import { adapter, type ReturnValidateToken } from "./auth";
|
||||
|
||||
export const luciaToken = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
attributes: {
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
sessionExpiresIn: new TimeSpan(365, "d"),
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
email: attributes.email,
|
||||
rol: attributes.rol,
|
||||
secret: attributes.secret !== null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const validateBearerToken = async (
|
||||
req: IncomingMessage,
|
||||
): ReturnValidateToken => {
|
||||
const authorizationHeader = req.headers.authorization;
|
||||
const sessionId = luciaToken.readBearerToken(authorizationHeader ?? "");
|
||||
if (!sessionId) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
const result = await luciaToken.validateSession(sessionId);
|
||||
return {
|
||||
session: result.session,
|
||||
...((result.user && {
|
||||
user: {
|
||||
authId: result.user.id,
|
||||
email: result.user.email,
|
||||
rol: result.user.rol,
|
||||
id: result.user.id,
|
||||
secret: result.user.secret,
|
||||
},
|
||||
}) || {
|
||||
user: null,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { generateAppName } from "./utils";
|
||||
import { registry } from "./registry";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||
|
||||
@@ -309,6 +310,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ export const auth = pgTable("auth", {
|
||||
rol: roles("rol").notNull(),
|
||||
image: text("image").$defaultFn(() => generateRandomImage()),
|
||||
secret: text("secret"),
|
||||
token: text("token"),
|
||||
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
|
||||
@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { mounts } from "./mount";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
@@ -79,6 +80,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
description: true,
|
||||
projectId: true,
|
||||
composeType: true,
|
||||
appName: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
|
||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mariadb = pgTable("mariadb", {
|
||||
mariadbId: text("mariadbId")
|
||||
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
export const apiCreateMariaDB = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
projectId: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mongo = pgTable("mongo", {
|
||||
mongoId: text("mongoId")
|
||||
@@ -73,6 +74,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
export const apiCreateMongo = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const mysql = pgTable("mysql", {
|
||||
mysqlId: text("mysqlId")
|
||||
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
export const apiCreateMySql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
||||
import { backups } from "./backups";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const postgres = pgTable("postgres", {
|
||||
postgresId: text("postgresId")
|
||||
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
|
||||
export const apiCreatePostgres = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { projects } from "./project";
|
||||
import { mounts } from "./mount";
|
||||
import { generateAppName } from "./utils";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const redis = pgTable("redis", {
|
||||
redisId: text("redisId")
|
||||
@@ -69,12 +70,12 @@ const createSchema = createInsertSchema(redis, {
|
||||
export const apiCreateRedis = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
projectId: true,
|
||||
description: true,
|
||||
})
|
||||
|
||||
.required();
|
||||
|
||||
export const apiFindOneRedis = createSchema
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { auth } from "./auth";
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
// export const sessionTable = sqliteTable("session", {
|
||||
// id: text("id").notNull().primaryKey(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users.id),
|
||||
// expiresAt: integer("expires_at").notNull(),
|
||||
// });
|
||||
export const sessionTable = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
|
||||
@@ -32,6 +32,7 @@ export const users = pgTable("user", {
|
||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
|
||||
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
|
||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -105,6 +106,7 @@ export const apiAssignPermissions = createSchema
|
||||
accesedServices: true,
|
||||
canAccessToTraefikFiles: true,
|
||||
canAccessToDocker: true,
|
||||
canAccessToAPI: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user