mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-30 03:25:22 +02: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
|
*.lockb
|
||||||
*.rdb
|
*.rdb
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -39,10 +39,17 @@ curl -sSL https://dokploy.com/install.sh | sh
|
|||||||
|
|
||||||
Getestete Systems:
|
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
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 Dokumentation
|
## 📄 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
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 Документация
|
## 📄 Документация
|
||||||
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).
|
Для подробной документации посетите [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
|
- Debian 11
|
||||||
- Fedora 40
|
- Fedora 40
|
||||||
- Centos 9
|
- Centos 9
|
||||||
|
- Centos 8
|
||||||
|
|
||||||
|
|
||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
|
|||||||
91
README.md
91
README.md
@@ -1,36 +1,37 @@
|
|||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h1 align="center">Dokploy</h1>
|
<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>
|
||||||
|
|
||||||
<div align="center" style="width:100%;">
|
|
||||||
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
|
|
||||||
</div>
|
</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.
|
||||||
|
|
||||||
|
|
||||||
|
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||||
## Explanation
|
* **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
||||||
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
|
* **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
|
## 🚀 Getting Started
|
||||||
|
|
||||||
To get started run the following command in a VPS:
|
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
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Tested Systems:
|
|
||||||
|
|
||||||
- Ubuntu 20.04
|
|
||||||
- Debian 11
|
|
||||||
- Fedora 40
|
|
||||||
- Centos 9
|
|
||||||
|
|
||||||
## 📄 Documentation
|
## 📄 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" />
|
<SelectValue placeholder="Select a protocol" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent defaultValue={"none"}>
|
<SelectContent>
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
<SelectItem value={"tcp"}>TCP</SelectItem>
|
||||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
<SelectItem value={"udp"}>UDP</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -58,6 +58,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -81,7 +82,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
traefikConfig: data || "",
|
traefikConfig: data || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [data]);
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateTraefikConfig) => {
|
const onSubmit = async (data: UpdateTraefikConfig) => {
|
||||||
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
|
||||||
@@ -100,6 +101,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik config Updated");
|
toast.success("Traefik config Updated");
|
||||||
refetch();
|
refetch();
|
||||||
|
setOpen(false);
|
||||||
|
form.reset();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the traefik config");
|
toast.error("Error to update the traefik config");
|
||||||
@@ -107,7 +110,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={(open) => {
|
||||||
|
setOpen(open)
|
||||||
|
if (!open) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button isLoading={isLoading}>Modify</Button>
|
<Button isLoading={isLoading}>Modify</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -122,7 +130,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form-update-traefik-config"
|
id="hook-form-update-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export const AddVolumes = ({
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="bind"
|
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
|
Bind Mount
|
||||||
</Label>
|
</Label>
|
||||||
@@ -209,7 +209,7 @@ export const AddVolumes = ({
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="volume"
|
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
|
Volume Mount
|
||||||
</Label>
|
</Label>
|
||||||
@@ -233,7 +233,7 @@ export const AddVolumes = ({
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="file"
|
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
|
File Mount
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -33,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.application.saveEnvironment.useMutation();
|
api.application.saveEnvironment.useMutation();
|
||||||
|
|
||||||
@@ -72,22 +74,57 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error to add environment");
|
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 (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your resource.
|
<CardDescription>
|
||||||
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
@@ -111,7 +149,12 @@ PORT=3000
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<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
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Application Deploying....");
|
toast.success("Deploying Application....");
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Application");
|
||||||
toast.success("Application Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
|
|||||||
import { StartApplication } from "../start-application";
|
import { StartApplication } from "../start-application";
|
||||||
import { StopApplication } from "../stop-application";
|
import { StopApplication } from "../stop-application";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
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 { DeployApplication } from "./deploy-application";
|
||||||
import { ResetApplication } from "./reset-application";
|
import { ResetApplication } from "./reset-application";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error to update Auto Deploy");
|
toast.error("Error to update Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
>
|
>
|
||||||
Autodeploy
|
Autodeploy
|
||||||
|
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||||
</Toggle>
|
</Toggle>
|
||||||
<RedbuildApplication applicationId={applicationId} />
|
<RedbuildApplication applicationId={applicationId} />
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
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 (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your resource.
|
<CardDescription>
|
||||||
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
@@ -109,7 +149,12 @@ PORT=3000
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end">
|
<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
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full relative gap-4"
|
className="w-full relative space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -99,8 +99,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
// disabled
|
// disabled
|
||||||
value={field.value}
|
value={field.value}
|
||||||
className="font-mono min-h-[20rem] compose-file-editor"
|
className="font-mono"
|
||||||
wrapperClassName="min-h-[20rem]"
|
wrapperClassName="compose-file-editor"
|
||||||
placeholder={`version: '3'
|
placeholder={`version: '3'
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
|||||||
@@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
composeStatus: "running",
|
composeStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Deploying....");
|
toast.success("Deploying Compose....");
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
composeId,
|
composeId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Compose");
|
||||||
toast.success("Compose Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Compose");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full relative z-[5]"
|
className="w-full relative z-[5]"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col overflow-auto">
|
<div className="flex flex-col overflow-auto">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
@@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Root Password</Label>
|
<Label>Root Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={data?.databaseRootPassword}
|
value={data?.databaseRootPassword}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database....");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
@@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
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 { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddTemplateSchema = z.object({
|
const AddTemplateSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
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(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddApplication = ({ projectId }: Props) => {
|
export const AddApplication = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const slug = slugify(projectName);
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.application.create.useMutation();
|
api.application.create.useMutation();
|
||||||
@@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
const form = useForm<AddTemplate>({
|
const form = useForm<AddTemplate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddTemplateSchema),
|
resolver: zodResolver(AddTemplateSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset();
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddTemplate) => {
|
const onSubmit = async (data: AddTemplate) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Created");
|
toast.success("Service Created");
|
||||||
|
form.reset();
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
toast.error("Error to create the service");
|
toast.error("Error to create the service");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<FormField
|
||||||
<FormField
|
control={form.control}
|
||||||
control={form.control}
|
name="name"
|
||||||
name="name"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input placeholder="Frontend" {...field} />
|
placeholder="Frontend"
|
||||||
</FormControl>
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
<FormMessage />
|
const val = e.target.value?.trim() || "";
|
||||||
</FormItem>
|
form.setValue("appName", `${slug}-${val}`);
|
||||||
)}
|
field.onChange(val);
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
@@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
|
|||||||
</FormItem>
|
</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>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -34,12 +34,22 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
const AddComposeSchema = z.object({
|
const AddComposeSchema = z.object({
|
||||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
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(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddCompose = ({ projectId }: Props) => {
|
export const AddCompose = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const slug = slugify(projectName);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
@@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
composeType: "docker-compose",
|
composeType: "docker-compose",
|
||||||
|
appName: `${slug}-`,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddComposeSchema),
|
resolver: zodResolver(AddComposeSchema),
|
||||||
});
|
});
|
||||||
@@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
projectId,
|
projectId,
|
||||||
composeType: data.composeType,
|
composeType: data.composeType,
|
||||||
|
appName: data.appName,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
@@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<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>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="composeType"
|
name="composeType"
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Database } from "lucide-react";
|
import { Database, AlertTriangle } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
|
|||||||
|
|
||||||
const baseDatabaseSchema = z.object({
|
const baseDatabaseSchema = z.object({
|
||||||
name: z.string().min(1, "Name required"),
|
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(),
|
databasePassword: z.string(),
|
||||||
dockerImage: z.string(),
|
dockerImage: z.string(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
@@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.merge(baseDatabaseSchema),
|
.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>;
|
type AddDatabase = z.infer<typeof mySchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabase = ({ projectId }: Props) => {
|
export const AddDatabase = ({ projectId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
|
const slug = slugify(projectName);
|
||||||
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
|
const redisMutation = api.redis.create.useMutation();
|
||||||
const { mutateAsync: createRedis } = api.redis.create.useMutation();
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
|
const mysqlMutation = api.mysql.create.useMutation();
|
||||||
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddDatabase>({
|
const form = useForm<AddDatabase>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
dockerImage: "",
|
dockerImage: "",
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: `${slug}-`,
|
||||||
databasePassword: "",
|
databasePassword: "",
|
||||||
description: "",
|
description: "",
|
||||||
databaseName: "",
|
databaseName: "",
|
||||||
@@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
const type = form.watch("type");
|
const type = form.watch("type");
|
||||||
|
const activeMutation = {
|
||||||
useEffect(() => {
|
postgres: postgresMutation,
|
||||||
form.reset({
|
mongo: mongoMutation,
|
||||||
type: "postgres",
|
redis: redisMutation,
|
||||||
dockerImage: "",
|
mariadb: mariadbMutation,
|
||||||
name: "",
|
mysql: mysqlMutation,
|
||||||
databasePassword: "",
|
};
|
||||||
description: "",
|
|
||||||
databaseName: "",
|
|
||||||
databaseUser: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddDatabase) => {
|
const onSubmit = async (data: AddDatabase) => {
|
||||||
const defaultDockerImage =
|
const defaultDockerImage =
|
||||||
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
|
||||||
|
|
||||||
let promise: Promise<unknown> | null = null;
|
let promise: Promise<unknown> | null = null;
|
||||||
|
const commonParams = {
|
||||||
|
name: data.name,
|
||||||
|
appName: data.appName,
|
||||||
|
dockerImage: defaultDockerImage,
|
||||||
|
projectId,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
|
||||||
if (data.type === "postgres") {
|
if (data.type === "postgres") {
|
||||||
promise = createPostgresql({
|
promise = postgresMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mongo") {
|
} else if (data.type === "mongo") {
|
||||||
promise = createMongo({
|
promise = mongoMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = createRedis({
|
promise = redisMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
projectId,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mariadb") {
|
} else if (data.type === "mariadb") {
|
||||||
promise = createMariadb({
|
promise = mariadbMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = createMysql({
|
promise = mysqlMutation.mutateAsync({
|
||||||
name: data.name,
|
...commonParams,
|
||||||
dockerImage: defaultDockerImage,
|
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseName: data.databaseName,
|
databaseName: data.databaseName,
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
projectId,
|
|
||||||
databaseRootPassword: data.databaseRootPassword,
|
databaseRootPassword: data.databaseRootPassword,
|
||||||
description: data.description,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
await promise
|
await promise
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Created");
|
toast.success("Database Created");
|
||||||
|
form.reset({
|
||||||
|
type: "postgres",
|
||||||
|
dockerImage: "",
|
||||||
|
name: "",
|
||||||
|
appName: `${projectName}-`,
|
||||||
|
databasePassword: "",
|
||||||
|
description: "",
|
||||||
|
databaseName: "",
|
||||||
|
databaseUser: "",
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
await utils.project.one.invalidate({
|
await utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
@@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
@@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<span>Database</span>
|
<span>Database</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</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>
|
<DialogHeader>
|
||||||
<DialogTitle>Databases</DialogTitle>
|
<DialogTitle>Databases</DialogTitle>
|
||||||
</DialogHeader>
|
</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 {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
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">
|
{Object.entries(databasesMap).map(([key, value]) => (
|
||||||
<FormControl className="w-full">
|
<FormItem
|
||||||
<div>
|
key={key}
|
||||||
<RadioGroupItem
|
className="flex w-full items-center space-x-3 space-y-0"
|
||||||
value="postgres"
|
>
|
||||||
id="postgres"
|
<FormControl className="w-full">
|
||||||
className="peer sr-only"
|
<div>
|
||||||
/>
|
<RadioGroupItem
|
||||||
<Label
|
value={key}
|
||||||
htmlFor="postgres"
|
id={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"
|
className="peer sr-only"
|
||||||
>
|
/>
|
||||||
<PostgresqlIcon />
|
<Label
|
||||||
Postgresql
|
htmlFor={key}
|
||||||
</Label>
|
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"
|
||||||
</div>
|
>
|
||||||
</FormControl>
|
{value.icon}
|
||||||
</FormItem>
|
{value.label}
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
</Label>
|
||||||
<FormControl className="w-full">
|
</div>
|
||||||
<div>
|
</FormControl>
|
||||||
<RadioGroupItem
|
</FormItem>
|
||||||
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>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<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>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<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>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>AppName</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 "
|
className="w-full space-y-4"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -50,17 +50,13 @@ export const DeployRedis = ({ redisId }: Props) => {
|
|||||||
applicationStatus: "running",
|
applicationStatus: "running",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Database Deploying....");
|
toast.success("Deploying Database...");
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
redisId,
|
redisId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(() => {
|
toast.error("Error to deploy Database");
|
||||||
toast.success("Database Deployed Succesfully");
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to deploy Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="grid w-full gap-8">
|
<div className="grid w-full gap-8">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Label>External Host</Label>
|
<Label>External Host</Label>
|
||||||
<Input disabled value={connectionUrl} />
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
@@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
type="password"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
<div className="flex flex-col gap-2 md:col-span-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<Input
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
|
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 = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
||||||
|
api.auth.generateToken.useMutation();
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: data?.email || "",
|
email: data?.email || "",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const ShowSettings = () => {
|
|||||||
<ShowCertificates />
|
<ShowCertificates />
|
||||||
<WebDomain />
|
<WebDomain />
|
||||||
<WebServer />
|
<WebServer />
|
||||||
|
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const addPermissions = z.object({
|
|||||||
canDeleteServices: z.boolean().optional().default(false),
|
canDeleteServices: z.boolean().optional().default(false),
|
||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
canAccessToDocker: z.boolean().optional().default(false),
|
||||||
|
canAccessToAPI: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPermissions = z.infer<typeof addPermissions>;
|
type AddPermissions = z.infer<typeof addPermissions>;
|
||||||
@@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||||
@@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
accesedProjects: data.accesedProjects || [],
|
accesedProjects: data.accesedProjects || [],
|
||||||
accesedServices: data.accesedServices || [],
|
accesedServices: data.accesedServices || [],
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Permissions updated");
|
toast.success("Permissions updated");
|
||||||
@@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accesedProjects"
|
name="accesedProjects"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const WebServer = () => {
|
|||||||
Space
|
Space
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent className="w-64" align="start">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form-update-main-traefik-config"
|
id="hook-form-update-main-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full py-4 relative"
|
className="w-full space-y-4 relative"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form-update-server-traefik-config"
|
id="hook-form-update-server-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
|||||||
<form
|
<form
|
||||||
id="hook-form-update-server-traefik-config"
|
id="hook-form-update-server-traefik-config"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
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">
|
<div className="flex flex-col">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const CodeEditor = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{props.disabled && (
|
{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>
|
</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
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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">
|
<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" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<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,
|
"when": 1716715367982,
|
||||||
"tag": "0014_same_hammerhead",
|
"tag": "0014_same_hammerhead",
|
||||||
"breakpoints": true
|
"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",
|
"name": "dokploy",
|
||||||
"version": "v0.2.0",
|
"version": "v0.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -31,11 +31,11 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language":"^6.10.1",
|
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
"@aws-sdk/client-s3": "3.515.0",
|
||||||
"@codemirror/legacy-modes":"6.4.0",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
|
"@codemirror/language": "^6.10.1",
|
||||||
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
"dockerode": "4.0.2",
|
"dockerode": "4.0.2",
|
||||||
"dockerode-compose": "^1.4.0",
|
"dockerode-compose": "^1.4.0",
|
||||||
@@ -105,17 +106,18 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
"recharts": "^2.12.3",
|
"recharts": "^2.12.3",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
"swagger-ui-react": "^5.17.14",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tar-fs": "3.0.5",
|
"tar-fs": "3.0.5",
|
||||||
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"use-resize-observer": "9.1.0",
|
"use-resize-observer": "9.1.0",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4"
|
||||||
"copy-webpack-plugin": "^12.0.2"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.7.1",
|
"@biomejs/biome": "1.7.1",
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/tar-fs": "2.0.4",
|
"@types/tar-fs": "2.0.4",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
|
|||||||
|
|
||||||
// export API handler
|
// export API handler
|
||||||
export default createNextApiHandler({
|
export default createNextApiHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: createTRPCContext,
|
createContext: createTRPCContext,
|
||||||
onError:
|
onError:
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? ({ path, error }) => {
|
? ({ path, error }) => {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -210,9 +210,12 @@ const Project = (
|
|||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AddApplication projectId={projectId} />
|
<AddApplication
|
||||||
<AddDatabase projectId={projectId} />
|
projectId={projectId}
|
||||||
<AddCompose projectId={projectId} />
|
projectName={data?.name}
|
||||||
|
/>
|
||||||
|
<AddDatabase projectId={projectId} projectName={data?.name} />
|
||||||
|
<AddCompose projectId={projectId} projectName={data?.name} />
|
||||||
<AddTemplate projectId={projectId} />
|
<AddTemplate projectId={projectId} />
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
|
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
|
||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { validateRequest } from "@/server/auth/auth";
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
|
||||||
const Page = () => {
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
|
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||||
</div>
|
</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 { composeRouter } from "./routers/compose";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
import { clusterRouter } from "./routers/cluster";
|
import { clusterRouter } from "./routers/cluster";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
*
|
*
|
||||||
@@ -39,6 +40,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
redis: redisRouter,
|
redis: redisRouter,
|
||||||
mongo: mongoRouter,
|
mongo: mongoRouter,
|
||||||
mariadb: mariadbRouter,
|
mariadb: mariadbRouter,
|
||||||
|
compose: composeRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
domain: domainRouter,
|
domain: domainRouter,
|
||||||
destination: destinationRouter,
|
destination: destinationRouter,
|
||||||
@@ -50,7 +52,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
security: securityRouter,
|
security: securityRouter,
|
||||||
redirects: redirectsRouter,
|
redirects: redirectsRouter,
|
||||||
port: portRouter,
|
port: portRouter,
|
||||||
compose: composeRouter,
|
|
||||||
registry: registryRouter,
|
registry: registryRouter,
|
||||||
cluster: clusterRouter,
|
cluster: clusterRouter,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewService(ctx.user.authId, newApplication.applicationId);
|
await addNewService(ctx.user.authId, newApplication.applicationId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the application",
|
message: "Error to create the application",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
updateAuthById,
|
updateAuthById,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "../services/auth";
|
} from "../services/auth";
|
||||||
|
import { luciaToken } from "@/server/auth/token";
|
||||||
|
|
||||||
export const authRouter = createTRPCRouter({
|
export const authRouter = createTRPCRouter({
|
||||||
createAdmin: publicProcedure
|
createAdmin: publicProcedure
|
||||||
@@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
|
|||||||
return auth;
|
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 }) => {
|
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
|
||||||
const auth = await findAuthById(input.id);
|
const auth = await findAuthById(input.id);
|
||||||
return auth;
|
return auth;
|
||||||
@@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
|
verifyToken: protectedProcedure.mutation(async () => {
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
|
|||||||
import { removeDeploymentsByComposeId } from "../services/deployment";
|
import { removeDeploymentsByComposeId } from "../services/deployment";
|
||||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { createCommand } from "@/server/utils/builders/compose";
|
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 { findAdmin } from "../services/admin";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { findProjectById, slugifyProjectName } from "../services/project";
|
import { findProjectById } from "../services/project";
|
||||||
import { createMount } from "../services/mount";
|
import { createMount } from "../services/mount";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
|
import { slugify } from "@/lib/slug";
|
||||||
|
|
||||||
export const composeRouter = createTRPCRouter({
|
export const composeRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const project = await findProjectById(input.projectId);
|
const project = await findProjectById(input.projectId);
|
||||||
|
|
||||||
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const { envs, mounts } = generate({
|
const { envs, mounts } = generate({
|
||||||
serverIp: admin.serverIp,
|
serverIp: admin.serverIp,
|
||||||
projectName: projectName,
|
projectName: projectName,
|
||||||
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
env: envs.join("\n"),
|
env: envs.join("\n"),
|
||||||
name: input.id,
|
name: input.id,
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
|
appName: `${projectName}-${generatePassword(6)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mariadb database",
|
message: "Error input: Inserting mariadb database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mongo database",
|
message: "Error input: Inserting mongo database",
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting mysql database",
|
message: "Error input: Inserting mysql database",
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting postgresql database",
|
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 { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateProject,
|
apiCreateProject,
|
||||||
@@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneProject)
|
.input(apiFindOneProject)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { manageRegistry } from "@/server/utils/traefik/registry";
|
import { manageRegistry } from "@/server/utils/traefik/registry";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
import { docker } from "@/server/constants";
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
export const registryRouter = createTRPCRouter({
|
export const registryRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
@@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
|
|||||||
.input(apiTestRegistry)
|
.input(apiTestRegistry)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const result = await docker.checkAuth({
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
username: input.username,
|
await execAsync(loginCommand);
|
||||||
password: input.password,
|
|
||||||
serveraddress: input.registryUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log("Error Registry:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
} from "../services/settings";
|
} from "../services/settings";
|
||||||
import { canAccessToTraefikFiles } from "../services/user";
|
import { canAccessToTraefikFiles } from "../services/user";
|
||||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||||
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
|
import { appRouter } from "../root";
|
||||||
|
|
||||||
export const settingsRouter = createTRPCRouter({
|
export const settingsRouter = createTRPCRouter({
|
||||||
reloadServer: adminProcedure.mutation(async () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
@@ -242,5 +244,50 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return readConfigInPath(input.path);
|
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 { createTraefikConfig } from "@/server/utils/traefik/application";
|
||||||
import { docker } from "@/server/constants";
|
import { docker } from "@/server/constants";
|
||||||
import { getAdvancedStats } from "@/server/monitoring/utilts";
|
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 type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
input: typeof apiCreateApplication._type,
|
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) => {
|
return await db.transaction(async (tx) => {
|
||||||
const newApplication = await tx
|
const newApplication = await tx
|
||||||
.insert(applications)
|
.insert(applications)
|
||||||
|
|||||||
@@ -13,10 +13,25 @@ import { join } from "node:path";
|
|||||||
import { COMPOSE_PATH } from "@/server/constants";
|
import { COMPOSE_PATH } from "@/server/constants";
|
||||||
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
import { cloneGithubRepository } from "@/server/utils/providers/github";
|
||||||
import { cloneGitRepository } from "@/server/utils/providers/git";
|
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 type Compose = typeof compose.$inferSelect;
|
||||||
|
|
||||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
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
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
@@ -39,6 +54,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
export const createComposeByTemplate = async (
|
export const createComposeByTemplate = async (
|
||||||
input: typeof compose.$inferInsert,
|
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
|
const newDestination = await db
|
||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ export const getContainers = async () => {
|
|||||||
.filter((container) => !container.name.includes("dokploy"));
|
.filter((container) => !container.name.includes("dokploy"));
|
||||||
|
|
||||||
return containers;
|
return containers;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfig = async (containerId: string) => {
|
export const getConfig = async (containerId: string) => {
|
||||||
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
|
|||||||
const config = JSON.parse(stdout);
|
const config = JSON.parse(stdout);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainersByAppNameMatch = async (appName: string) => {
|
export const getContainersByAppNameMatch = async (appName: string) => {
|
||||||
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error(`Execution error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,26 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
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 type Mariadb = typeof mariadb.$inferSelect;
|
||||||
|
|
||||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
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
|
const newMariadb = await db
|
||||||
.insert(mariadb)
|
.insert(mariadb)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,26 @@ import { buildMongo } from "@/server/utils/databases/mongo";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
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 type Mongo = typeof mongo.$inferSelect;
|
||||||
|
|
||||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
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
|
const newMongo = await db
|
||||||
.insert(mongo)
|
.insert(mongo)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,11 +5,27 @@ import { buildMysql } from "@/server/utils/databases/mysql";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
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 type MySql = typeof mysql.$inferSelect;
|
||||||
|
|
||||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
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
|
const newMysql = await db
|
||||||
.insert(mysql)
|
.insert(mysql)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -5,10 +5,26 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
|
|||||||
import { pullImage } from "@/server/utils/docker/utils";
|
import { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
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 type Postgres = typeof postgres.$inferSelect;
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
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
|
const newPostgres = await db
|
||||||
.insert(postgres)
|
.insert(postgres)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { db } from "@/server/db";
|
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 { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { findAdmin } from "./admin";
|
import { findAdmin } from "./admin";
|
||||||
@@ -75,12 +84,40 @@ export const updateProjectById = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const slugifyProjectName = (projectName: string): string => {
|
export const validUniqueServerAppName = async (appName: string) => {
|
||||||
return projectName
|
const query = await db.query.projects.findMany({
|
||||||
.toLowerCase()
|
with: {
|
||||||
.replace(/[0-9]/g, "")
|
applications: {
|
||||||
.replace(/[^a-z\s-]/g, "")
|
where: eq(applications.appName, appName),
|
||||||
.replace(/\s+/g, "-")
|
},
|
||||||
.replace(/-+/g, "-")
|
mariadb: {
|
||||||
.replace(/^-+|-+$/g, "");
|
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 { pullImage } from "@/server/utils/docker/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
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;
|
export type Redis = typeof redis.$inferSelect;
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
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
|
const newRedis = await db
|
||||||
.insert(redis)
|
.insert(redis)
|
||||||
.values({
|
.values({
|
||||||
|
|||||||
@@ -9,28 +9,35 @@ import {
|
|||||||
} from "@/server/utils/traefik/registry";
|
} from "@/server/utils/traefik/registry";
|
||||||
import { removeService } from "@/server/utils/docker/utils";
|
import { removeService } from "@/server/utils/docker/utils";
|
||||||
import { initializeRegistry } from "@/server/setup/registry-setup";
|
import { initializeRegistry } from "@/server/setup/registry-setup";
|
||||||
|
import { execAsync } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
export type Registry = typeof registry.$inferSelect;
|
export type Registry = typeof registry.$inferSelect;
|
||||||
|
|
||||||
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
|
|
||||||
const newRegistry = await db
|
return await db.transaction(async (tx) => {
|
||||||
.insert(registry)
|
const newRegistry = await tx
|
||||||
.values({
|
.insert(registry)
|
||||||
...input,
|
.values({
|
||||||
adminId: admin.adminId,
|
...input,
|
||||||
})
|
adminId: admin.adminId,
|
||||||
.returning()
|
})
|
||||||
.then((value) => value[0]);
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
if (!newRegistry) {
|
if (!newRegistry) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error input: Inserting registry",
|
message: "Error input: Inserting registry",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return newRegistry;
|
|
||||||
|
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) => {
|
export const removeRegistry = async (registryId: string) => {
|
||||||
@@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
|
|||||||
await removeService("dokploy-registry");
|
await removeService("dokploy-registry");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await execAsync(`docker logout ${response.registryUrl}`);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import superjson from "superjson";
|
|||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
import { validateRequest } from "../auth/auth";
|
import { validateRequest } from "../auth/auth";
|
||||||
import type { Session, User } from "lucia";
|
import type { Session, User } from "lucia";
|
||||||
|
import { validateBearerToken } from "../auth/token";
|
||||||
|
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
@@ -59,9 +61,15 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||||
const { req, res } = opts;
|
const { req, res } = opts;
|
||||||
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
|
||||||
const { session, user } = await validateRequest(req, res);
|
let { session, user } = await validateBearerToken(req);
|
||||||
user;
|
|
||||||
|
if (!session) {
|
||||||
|
const cookieResult = await validateRequest(req, res);
|
||||||
|
session = cookieResult.session;
|
||||||
|
user = cookieResult.user;
|
||||||
|
}
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -88,19 +96,22 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
* errors on the backend.
|
* errors on the backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
const t = initTRPC
|
||||||
transformer: superjson,
|
.meta<OpenApiMeta>()
|
||||||
errorFormatter({ shape, error }) {
|
.context<typeof createTRPCContext>()
|
||||||
return {
|
.create({
|
||||||
...shape,
|
transformer: superjson,
|
||||||
data: {
|
errorFormatter({ shape, error }) {
|
||||||
...shape.data,
|
return {
|
||||||
zodError:
|
...shape,
|
||||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
data: {
|
||||||
},
|
...shape.data,
|
||||||
};
|
zodError:
|
||||||
},
|
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
});
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
* 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 }) => {
|
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
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(
|
export async function validateRequest(
|
||||||
req: IncomingMessage,
|
req: IncomingMessage,
|
||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
): Promise<{
|
): ReturnValidateToken {
|
||||||
user: (User & { authId: string }) | null;
|
|
||||||
session: Session | null;
|
|
||||||
}> {
|
|
||||||
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return {
|
return {
|
||||||
user: null,
|
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";
|
} from "drizzle-orm/pg-core";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
|
||||||
|
|
||||||
@@ -309,6 +310,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
description: true,
|
description: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const auth = pgTable("auth", {
|
|||||||
rol: roles("rol").notNull(),
|
rol: roles("rol").notNull(),
|
||||||
image: text("image").$defaultFn(() => generateRandomImage()),
|
image: text("image").$defaultFn(() => generateRandomImage()),
|
||||||
secret: text("secret"),
|
secret: text("secret"),
|
||||||
|
token: text("token"),
|
||||||
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
|
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
|
|||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||||
"git",
|
"git",
|
||||||
@@ -79,6 +80,7 @@ export const apiCreateCompose = createSchema.pick({
|
|||||||
description: true,
|
description: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
composeType: true,
|
composeType: true,
|
||||||
|
appName: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateComposeByTemplate = createSchema
|
export const apiCreateComposeByTemplate = createSchema
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mariadb = pgTable("mariadb", {
|
export const mariadb = pgTable("mariadb", {
|
||||||
mariadbId: text("mariadbId")
|
mariadbId: text("mariadbId")
|
||||||
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
|
|||||||
export const apiCreateMariaDB = createSchema
|
export const apiCreateMariaDB = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
databaseRootPassword: true,
|
databaseRootPassword: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mongo = pgTable("mongo", {
|
export const mongo = pgTable("mongo", {
|
||||||
mongoId: text("mongoId")
|
mongoId: text("mongoId")
|
||||||
@@ -73,6 +74,7 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
export const apiCreateMongo = createSchema
|
export const apiCreateMongo = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const mysql = pgTable("mysql", {
|
export const mysql = pgTable("mysql", {
|
||||||
mysqlId: text("mysqlId")
|
mysqlId: text("mysqlId")
|
||||||
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
|
|||||||
export const apiCreateMySql = createSchema
|
export const apiCreateMySql = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { projects } from "./project";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const postgres = pgTable("postgres", {
|
export const postgres = pgTable("postgres", {
|
||||||
postgresId: text("postgresId")
|
postgresId: text("postgresId")
|
||||||
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
|
|||||||
export const apiCreatePostgres = createSchema
|
export const apiCreatePostgres = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databaseName: true,
|
databaseName: true,
|
||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const redis = pgTable("redis", {
|
export const redis = pgTable("redis", {
|
||||||
redisId: text("redisId")
|
redisId: text("redisId")
|
||||||
@@ -69,12 +70,12 @@ const createSchema = createInsertSchema(redis, {
|
|||||||
export const apiCreateRedis = createSchema
|
export const apiCreateRedis = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
appName: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
dockerImage: true,
|
dockerImage: true,
|
||||||
projectId: true,
|
projectId: true,
|
||||||
description: true,
|
description: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export const apiFindOneRedis = createSchema
|
export const apiFindOneRedis = createSchema
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
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", {
|
export const sessionTable = pgTable("session", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
userId: text("user_id")
|
userId: text("user_id")
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const users = pgTable("user", {
|
|||||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||||
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
|
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
|
||||||
|
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
|
||||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
@@ -105,6 +106,7 @@ export const apiAssignPermissions = createSchema
|
|||||||
accesedServices: true,
|
accesedServices: true,
|
||||||
canAccessToTraefikFiles: true,
|
canAccessToTraefikFiles: true,
|
||||||
canAccessToDocker: true,
|
canAccessToDocker: true,
|
||||||
|
canAccessToAPI: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user