Compare commits

..

74 Commits

Author SHA1 Message Date
Mauricio Siu
e01d92d1d9 Merge pull request #161 from Dokploy/canary
v0.2.4
2024-06-23 19:40:45 -06:00
Mauricio Siu
8f0a4f0886 chore: bump version 2024-06-23 19:32:11 -06:00
Mauricio Siu
01794c7742 fix: correct url swagger api 2024-06-23 19:21:40 -06:00
Mauricio Siu
19bbe69ee3 Merge pull request #159 from Dokploy/refactor/update-trpc-openapi
Refactor/update trpc openapi
2024-06-23 13:17:23 -06:00
Mauricio Siu
92c4e769ab refactor: add tags to openAPI 2024-06-23 13:08:52 -06:00
Mauricio Siu
0801e91816 refactor: add dokploy trpc open api 2024-06-23 12:17:38 -06:00
Mauricio Siu
b360cc2af4 Merge pull request #158 from Dokploy/57-dokploy-api-or-cli
57 dokploy api or cli
2024-06-23 00:28:39 -06:00
Mauricio Siu
ba5d8feba2 remove file 2024-06-23 00:26:30 -06:00
Mauricio Siu
43be9d0171 refactor: remove console log 2024-06-22 23:49:04 -06:00
Mauricio Siu
a6bbf5d96b chore: remove unused dependencie 2024-06-22 22:59:06 -06:00
Mauricio Siu
7e6e43adfe remove 2024-06-22 22:57:52 -06:00
Mauricio Siu
e8a4611ab7 refactor: fix styles 2024-06-22 22:57:10 -06:00
Mauricio Siu
608db3d401 feat: add no expiration to token generated 2024-06-22 22:38:01 -06:00
Mauricio Siu
0add62f14d refactor: restrict swagger api by user access 2024-06-22 21:45:09 -06:00
Mauricio Siu
1754f63352 feat: add token access to user 2024-06-22 20:45:29 -06:00
Mauricio Siu
edcc7544fb feat: add access to API/CLI support 2024-06-22 20:23:06 -06:00
Mauricio Siu
1edf30546d refactor: remove swagger from server startup 2024-06-22 20:18:22 -06:00
Mauricio Siu
ad806437af feat: add openapi and swagger support 2024-06-22 20:17:55 -06:00
Mauricio Siu
f0eecf354b refactor: remove input 2024-06-22 18:27:16 -06:00
Mauricio Siu
c5ace67b3f Merge branch 'canary' into 57-dokploy-api-or-cli 2024-06-21 21:49:41 -06:00
Mauricio Siu
fe22890311 Merge pull request #156 from Dokploy/canary
v0.2.3
2024-06-21 11:50:40 -06:00
Mauricio Siu
3f2eeaf386 chore: bump version 2024-06-21 10:37:26 -06:00
Mauricio Siu
7826ba5bb5 Merge pull request #150 from Dokploy/149-backup-db-mongodb-to-s3-errors
fix(#149): use database user in mongodump
2024-06-19 00:49:32 -06:00
Mauricio Siu
bdc488e179 fix(#149): use database user in mongodump 2024-06-19 00:45:33 -06:00
Mauricio Siu
fc2abac989 chore: update banner image 2024-06-15 19:27:31 -06:00
Mauricio Siu
101bbd44d8 Update README.md 2024-06-15 04:08:32 -06:00
Mauricio Siu
68c2272e98 chore: update readme 2024-06-15 04:02:24 -06:00
Mauricio Siu
bc28464430 chore: update readme 2024-06-15 04:01:35 -06:00
Mauricio Siu
7df415a386 chore: add issue template 2024-06-15 03:15:02 -06:00
Mauricio Siu
9fbd3039a6 Merge pull request #141 from hehehai/hehehai/fix-env-editor-width-over
fix: env editor width overflow
2024-06-15 03:03:13 -06:00
hehehai
3c00937b94 fix: env editor width overflow 2024-06-13 20:42:46 +08:00
Mauricio Siu
ae4226531e Merge branch 'canary' into 57-dokploy-api-or-cli 2024-06-12 00:23:53 -06:00
Mauricio Siu
a57a776500 Merge pull request #137 from dharsanb/canary
Fix link to docs in README
2024-06-09 14:46:24 -06:00
dharsanb
323e2f54ba fix(readme): fixed readme link to docs 2024-06-10 01:29:19 +05:30
Mauricio Siu
2b7c7632f4 Merge pull request #136 from Dokploy/canary
v0.2.2
2024-06-08 22:06:39 -06:00
Mauricio Siu
7db662e332 chore: bump version 2024-06-08 21:17:55 -06:00
Mauricio Siu
cd1a686b59 refactor: update slugify compose 2024-06-08 18:40:34 -06:00
Mauricio Siu
78d573c4f3 Merge pull request #87 from hehehai/hehehai/feat-server-custom-name
feat: server support custom name
2024-06-08 16:49:15 -06:00
Mauricio Siu
0ef9b1427b chore: add slugify 2024-06-08 16:43:06 -06:00
Mauricio Siu
993d6b52f2 refactor: remove comments 2024-06-08 16:43:01 -06:00
Mauricio Siu
b9ab4a4d1a refactor: remove console log 2024-06-08 16:42:50 -06:00
Mauricio Siu
83153471b8 feat: add docker compose appName validation 2024-06-08 16:42:38 -06:00
Mauricio Siu
909e536f45 refactor: hide enviroment variables when viewing enviroment variables 2024-06-08 16:42:07 -06:00
Mauricio Siu
295cf50060 feat: add slug function 2024-06-08 16:41:46 -06:00
Mauricio Siu
dd16baf234 refactor: use slugify function 2024-06-08 16:41:38 -06:00
Mauricio Siu
72c366aa10 refactor: slugify and add hash to appName 2024-06-08 16:41:05 -06:00
Mauricio Siu
9a4f79f9e6 Merge branch 'canary' into hehehai/feat-server-custom-name 2024-06-08 14:37:51 -06:00
Mauricio Siu
30b81834fc Merge pull request #135 from Dokploy/130-how-to-make-dokploy-to-do-not-require-port-80-and-443-to-be-avilable
feat(#130): allow to pass enviroment variables to assign custom port …
2024-06-08 14:19:14 -06:00
Mauricio Siu
4e3aaa2a69 feat(#130): allow to pass enviroment variables to assign custom port on traefik 2024-06-08 14:13:43 -06:00
Mauricio Siu
3dcd89cc32 Merge pull request #128 from mariusihring/canary
Updated the database view to hide the password in the connection string
2024-06-08 14:05:16 -06:00
Mauricio Siu
41dc388bb0 refactor: update import 2024-06-08 13:40:15 -06:00
Mauricio Siu
44a592f7a7 refactor: simplify props 2024-06-08 13:36:02 -06:00
Mauricio Siu
1a4f5607dc refactor: add props to toggle and change colors of button toggle 2024-06-08 13:28:43 -06:00
Marius
d54c6e4ac9 feat: create input for toggleable inputs like passwords/connectinstrings 2024-06-08 11:55:05 +02:00
Marius
936cf76a4c fix: fix undefined error for database length 2024-06-07 13:04:30 +02:00
Marius
65254f1686 feat: Update the external database view to hide the password 2024-06-07 12:57:46 +02:00
Marius
b312b1d7e0 Updated the database view to hide the password in the connection string 2024-06-07 12:45:33 +02:00
Mauricio Siu
1b7244e841 Merge pull request #127 from Dokploy/canary
v0.2.1
2024-06-07 02:52:03 -06:00
Mauricio Siu
7eb1716d71 refactor: add docker config to improve UX 2024-06-07 01:27:21 -06:00
Mauricio Siu
b8a8e32659 refactor(#125): logout from docker when deleting the registry 2024-06-07 00:50:02 -06:00
Mauricio Siu
eea00d28cd refactor(#125): don't add the registry if the login already exists 2024-06-07 00:26:01 -06:00
Mauricio Siu
449a61e208 Merge branch 'main' into canary 2024-06-07 00:14:12 -06:00
Mauricio Siu
037f3ed118 chore: bump version 2024-06-07 00:11:22 -06:00
Mauricio Siu
471802c4da Merge pull request #126 from Dokploy/125-private-docker-image-with-docker-compose-issue
fix(#125): Use exec async and password stnd to automatically save in …
2024-06-07 00:11:04 -06:00
Mauricio Siu
502ff638d6 fix(#125): Use exec async and password stnd to automatically save in .docker/config.json 2024-06-07 00:06:11 -06:00
Mauricio Siu
750c12ded5 Merge pull request #122 from Dokploy/118-toast-indicates-success-even-for-failed-deployments
refacto(#118): remove last toaster on start deployments
2024-06-06 00:13:05 -06:00
Mauricio Siu
7fb34ade00 refacto(#118): remove last toaster on start deployments 2024-06-06 00:11:46 -06:00
Mauricio Siu
0c197c095b Merge pull request #121 from Dokploy/119-remove-none-as-protocol-option-for-ports
fix(#119): remove none ports
2024-06-05 22:49:12 -06:00
Mauricio Siu
4cbff731d1 fix(#119): remove none ports 2024-06-05 22:47:28 -06:00
Mauricio Siu
b9bff95c3d feat: wip cli token authentication 2024-06-05 22:42:11 -06:00
Mr1Blaze
113df9ae12 Added New Tested Systems (#116)
Co-authored-by: Mr1Blaze <imarshall@pro.pro>
2024-06-03 20:52:37 -06:00
Mauricio Siu
7f13fd24ec v0.2.0 (#117)
* feat: add schema for registry and routes

* feat: add docker registry upload

* feat: add show cluster

* refactor: set the registry url in image in case we have a registry asociated

* feat: add update registry and fix the docker url markup

* chore: remove --advertise-ip on swarm script

* refactor: remove listen address of swarm initialize

* feat: add table to show nodes and add dropdown to add manager & workers

* refactor: improve interface for cluster

* refactor: improve UI

* feat: add experimental swarm settings

* refactor: remove comments

* refactor: prettify json of each setting

* refactor: add interface tooltip

* refactor: delete static form self registry

* refactor: allow to se a empty registry

* fix: remove text area warnings

* feat: add network swarm json

* refactor: update ui

* revert: go back to swarm init config

* refactor: remove initialization on server, only on setup script

* Update LICENSE.MD

* feat: appearance theme support system config

* refactor: remove logs

* fix(README-ru): hyperlink-ed docs url

* feat: (#107) webhook listener filter docker events based on image tag.

Fixes #107

* refactor: simplify comparison docker tags

* refactor: remove return in res status

* refactor: prevent to updates download automatically

* feat: support code editor (#105)

* feat: support code editor

* Update codeblock

* refactor: remove unused class

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* fix: select the right image from sourcetype (#109)

* chore: bump minor version

* fix: add redirect to https by default (#113)

* Create FUNDING.yml

* Docker compose support (#111)

* feat(WIP): compose implementation

* feat: add volumes, networks, services name hash generate

* feat: add compose config test unique

* feat: add tests for each unique config

* feat: implement lodash for docker compose parsing

* feat: add tests for generating compose file

* refactor: implement logs docker compose

* refactor: composeFile set not empty

* feat: implement providers for compose deployments

* feat: add Files volumes to compose

* feat: add stop compose button

* refactor: change strategie of building compose

* feat: create .env file in composepath

* refactor: simplify git and github function

* chore: update deps

* refactor: update migrations and add badge to recognize compose type

* chore: update lock yaml

* refactor: use code editor

* feat: add monitoring for app types

* refactor: reset stats on change appName

* refactor: add option to clean monitoring folder

* feat: show current command that will run

* feat: add prefix

* fix: add missing types

* refactor: add docker provider and expose by default as false

* refactor: customize error page

* refactor: unified deployments to be a single one

* feat: add vitest to ci/cd

* revert: back to initial version

* refactor: add maxconcurrency vitest

* refactor: add pool forks to vitest

* feat: add pocketbase template

* fix: update path resolution compose

* removed

* feat: add template pocketbase

* feat: add pocketbase template

* feat: add support button

* feat: add plausible template

* feat: add calcom template

* feat: add version to each template

* feat: add code editor to enviroment variables and swarm settings json

* refactor: add loader when download the image

* fix: use base64 to generate keys plausible

* feat: add recognized domain names by enviroment compose

* refactor: show alert to redeploy in each card advanced tab

* refactor: add validation to prevent create compose if not have permissions

* chore: add templates section to contributing

* chore: add example contributing

* chore: add recomendation to show variables

* chore: add video to contributing templates

* chore: bump version

---------

Co-authored-by: hehehai <riverhohai@gmail.com>
Co-authored-by: Bayram Tagiev <bayram.tagiev.a@gmail.com>
Co-authored-by: Paulo Santana <30875229+hikinine@users.noreply.github.com>
2024-06-02 21:24:56 -06:00
Mauricio Siu
290228116a v0.1.0 (#114)
* feat: add schema for registry and routes

* feat: add docker registry upload

* feat: add show cluster

* refactor: set the registry url in image in case we have a registry asociated

* feat: add update registry and fix the docker url markup

* chore: remove --advertise-ip on swarm script

* refactor: remove listen address of swarm initialize

* feat: add table to show nodes and add dropdown to add manager & workers

* refactor: improve interface for cluster

* refactor: improve UI

* feat: add experimental swarm settings

* refactor: remove comments

* refactor: prettify json of each setting

* refactor: add interface tooltip

* refactor: delete static form self registry

* refactor: allow to se a empty registry

* fix: remove text area warnings

* feat: add network swarm json

* refactor: update ui

* revert: go back to swarm init config

* refactor: remove initialization on server, only on setup script

* Update LICENSE.MD

* feat: appearance theme support system config

* refactor: remove logs

* fix(README-ru): hyperlink-ed docs url

* feat: (#107) webhook listener filter docker events based on image tag.

Fixes #107

* refactor: simplify comparison docker tags

* refactor: remove return in res status

* refactor: prevent to updates download automatically

* feat: support code editor (#105)

* feat: support code editor

* Update codeblock

* refactor: remove unused class

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* fix: select the right image from sourcetype (#109)

* chore: bump minor version

* fix: add redirect to https by default (#113)

---------

Co-authored-by: hehehai <riverhohai@gmail.com>
Co-authored-by: Bayram Tagiev <bayram.tagiev.a@gmail.com>
Co-authored-by: Paulo Santana <30875229+hikinine@users.noreply.github.com>
2024-05-29 23:36:59 -06:00
hehehai
fae180f157 feat: server support custom name 2024-05-24 15:43:05 +08:00
107 changed files with 10841 additions and 560 deletions

62
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View 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
View 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.

View 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
View File

@@ -55,3 +55,4 @@ yarn-error.log*
*.lockb
*.rdb
.idea

View File

@@ -39,10 +39,17 @@ curl -sSL https://dokploy.com/install.sh | sh
Getestete Systems:
- Ubuntu 20.04
- Ubuntu 24.04 LTS (Noble Numbat)
- Ubuntu 23.10 (Mantic Minotaur)
- Ubuntu 22.04 LTS (Jammy Jellyfish)
- Ubuntu 20.04 LTS (Focal Fossa)
- Ubuntu 18.04 LTS (Bionic Beaver)
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8
## 📄 Dokumentation

View File

@@ -40,10 +40,17 @@ curl -sSL https://dokploy.com/install.sh | sh
Проверенные системы:
- Ubuntu 20.04
- Ubuntu 24.04 LTS (Noble Numbat)
- Ubuntu 23.10 (Mantic Minotaur)
- Ubuntu 22.04 LTS (Jammy Jellyfish)
- Ubuntu 20.04 LTS (Focal Fossa)
- Ubuntu 18.04 LTS (Bionic Beaver)
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8
## 📄 Документация
Для подробной документации посетите [docs.dokploy.com/docs](https://docs.dokploy.com).

View File

@@ -43,10 +43,17 @@ curl -sSL https://dokploy.com/install.sh | sh
经过测试的系统:
- Ubuntu 20.04
- Ubuntu 24.04 LTS (Noble Numbat)
- Ubuntu 23.10 (Mantic Minotaur)
- Ubuntu 22.04 LTS (Jammy Jellyfish)
- Ubuntu 20.04 LTS (Focal Fossa)
- Ubuntu 18.04 LTS (Bionic Beaver)
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8
## 📄 文档

View File

@@ -1,36 +1,37 @@
<div align="center">
<h1 align="center">Dokploy</h1>
<div>
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
</div>
<div align="center" style="width:100%;">
<img src="https://raw.githubusercontent.com/Dokploy/dokploy/main/logo.png" alt="Reflex Logo" style="width:60%;">
</div>
<hr>
<br />
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases using Docker and Traefik. Designed to enhance efficiency and security, Dokploy allows you to deploy your applications on any VPS.
Dokploy include multiples features to make your life easier.
## Explanation
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)
* **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
* **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
* **Backups**: Automate backups for databases to a external storage destination.
* **Docker Compose**: Native support for Docker Compose to manage complex applications.
* **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
* **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
* **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
* **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
* **Docker Management**: Easily deploy and manage Docker containers.
* **CLI (Soon⌛)**: Manage your applications and databases using the command line.
* **Self-Hosted**: Self-host Dokploy on your VPS.
## 🌟 Features
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.) with ease.
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis, and more.
- **Docker Management**: Easily deploy and manage Docker containers.
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage.
- **Database Backups**: Automate backups with support for multiple storage destinations.
## 🚀 Getting Started
To get started run the following command in a VPS:
@@ -40,14 +41,54 @@ To get started run the following command in a VPS:
curl -sSL https://dokploy.com/install.sh | sh
```
Tested Systems:
- Ubuntu 20.04
- Debian 11
- Fedora 40
- Centos 9
## 📄 Documentation
For detailed documentation, visit [docs.dokploy.com/docs](https://docs.dokploy.com).
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.webp" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a>
## Donations
If you like dokploy, and want to support the project to cover the costs of hosting, testing and development new features, you can donate to the project using the following link:
Thanks to all the supporters!
https://opencollective.com/dokploy
<a href="https://opencollective.com/dokploy"><img src="https://opencollective.com/dokploy/individuals.svg?width=890"></a>
## Contributors
<a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" />
</a>
## Support OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8
## Explanation
[English](README.md) | [中文](README-zh.md) | [Deutsch](README-de.md) | [Русский Язык](README-ru.md)

View File

@@ -183,8 +183,7 @@ export const AddPort = ({
<SelectValue placeholder="Select a protocol" />
</SelectTrigger>
</FormControl>
<SelectContent defaultValue={"none"}>
<SelectItem value={"none"}>None</SelectItem>
<SelectContent>
<SelectItem value={"tcp"}>TCP</SelectItem>
<SelectItem value={"udp"}>UDP</SelectItem>
</SelectContent>

View File

@@ -19,7 +19,7 @@ import {
import { api } from "@/utils/api";
import { AlertBlock } from "@/components/shared/alert-block";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -58,6 +58,7 @@ export const validateAndFormatYAML = (yamlText: string) => {
};
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
const [open, setOpen] = useState(false);
const { data, refetch } = api.application.readTraefikConfig.useQuery(
{
applicationId,
@@ -81,7 +82,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
}, [data]);
const onSubmit = async (data: UpdateTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
@@ -100,6 +101,8 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
.then(async () => {
toast.success("Traefik config Updated");
refetch();
setOpen(false);
form.reset();
})
.catch(() => {
toast.error("Error to update the traefik config");
@@ -107,7 +110,12 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
};
return (
<Dialog>
<Dialog open={open} onOpenChange={(open) => {
setOpen(open)
if (!open) {
form.reset();
}
}}>
<DialogTrigger asChild>
<Button isLoading={isLoading}>Modify</Button>
</DialogTrigger>
@@ -122,7 +130,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
<form
id="hook-form-update-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 overflow-auto"
className="w-full space-y-4 overflow-auto"
>
<div className="flex flex-col">
<FormField

View File

@@ -189,7 +189,7 @@ export const AddVolumes = ({
/>
<Label
htmlFor="bind"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
Bind Mount
</Label>
@@ -209,7 +209,7 @@ export const AddVolumes = ({
/>
<Label
htmlFor="volume"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
Volume Mount
</Label>
@@ -233,7 +233,7 @@ export const AddVolumes = ({
/>
<Label
htmlFor="file"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
File Mount
</Label>

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import {
Card,
CardContent,
@@ -19,8 +19,9 @@ import {
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { Textarea } from "@/components/ui/textarea";
import { Toggle } from "@/components/ui/toggle";
import { CodeEditor } from "@/components/shared/code-editor";
import { EyeIcon, EyeOffIcon } from "lucide-react";
const addEnvironmentSchema = z.object({
environment: z.string(),
@@ -33,6 +34,7 @@ interface Props {
}
export const ShowEnvironment = ({ applicationId }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } =
api.application.saveEnvironment.useMutation();
@@ -72,22 +74,57 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
toast.error("Error to add environment");
});
};
useEffect(() => {
if (isEnvVisible) {
if (data?.env) {
const maskedLines = data.env
.split("\n")
.map((line) => "*".repeat(line.length))
.join("\n");
form.reset({
environment: maskedLines,
});
} else {
form.reset({
environment: "",
});
}
} else {
form.reset({
environment: data?.env || "",
});
}
}, [form.reset, data, form, isEnvVisible]);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isEnvVisible}
onPressedChange={setIsEnvVisible}
>
{isEnvVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}
@@ -97,6 +134,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
<FormControl>
<CodeEditor
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
PORT=3000
`}
@@ -111,7 +149,12 @@ PORT=3000
/>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
<Button
disabled={isEnvVisible}
isLoading={isLoading}
className="w-fit"
type="submit"
>
Save
</Button>
</div>

View File

@@ -11,6 +11,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props {
@@ -51,18 +52,14 @@ export const DeployApplication = ({ applicationId }: Props) => {
applicationId,
})
.then(async () => {
toast.success("Application Deploying....");
toast.success("Deploying Application....");
await refetch();
await deploy({
applicationId,
})
.then(() => {
toast.success("Application Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Application");
});
}).catch(() => {
toast.error("Error to deploy Application");
});
await refetch();
})

View File

@@ -10,7 +10,7 @@ import { RedbuildApplication } from "../rebuild-application";
import { StartApplication } from "../start-application";
import { StopApplication } from "../stop-application";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { Terminal } from "lucide-react";
import { CheckCircle2, Terminal } from "lucide-react";
import { DeployApplication } from "./deploy-application";
import { ResetApplication } from "./reset-application";
interface Props {
@@ -55,8 +55,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
>
Autodeploy
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle>
<RedbuildApplication applicationId={applicationId} />
{data?.applicationStatus === "idle" ? (

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import {
Card,
CardContent,
@@ -20,6 +20,8 @@ import {
import { api } from "@/utils/api";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor";
import { Toggle } from "@/components/ui/toggle";
import { EyeIcon, EyeOffIcon } from "lucide-react";
const addEnvironmentSchema = z.object({
environment: z.string(),
@@ -32,6 +34,7 @@ interface Props {
}
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
@@ -71,21 +74,57 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
});
};
useEffect(() => {
if (isEnvVisible) {
if (data?.env) {
const maskedLines = data.env
.split("\n")
.map((line) => "*".repeat(line.length))
.join("\n");
form.reset({
environment: maskedLines,
});
} else {
form.reset({
environment: "",
});
}
} else {
form.reset({
environment: data?.env || "",
});
}
}, [form.reset, data, form, isEnvVisible]);
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isEnvVisible}
onPressedChange={setIsEnvVisible}
>
{isEnvVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}
@@ -95,6 +134,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
<FormControl>
<CodeEditor
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
PORT=3000
`}
@@ -109,7 +149,12 @@ PORT=3000
/>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
<Button
disabled={isEnvVisible}
isLoading={isLoading}
className="w-fit"
type="submit"
>
Save
</Button>
</div>

View File

@@ -87,7 +87,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full relative gap-4"
className="w-full relative space-y-4"
>
<FormField
control={form.control}
@@ -99,8 +99,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
<CodeEditor
// disabled
value={field.value}
className="font-mono min-h-[20rem] compose-file-editor"
wrapperClassName="min-h-[20rem]"
className="font-mono"
wrapperClassName="compose-file-editor"
placeholder={`version: '3'
services:
web:

View File

@@ -49,18 +49,14 @@ export const DeployCompose = ({ composeId }: Props) => {
composeStatus: "running",
})
.then(async () => {
toast.success("Compose Deploying....");
toast.success("Deploying Compose....");
await refetch();
await deploy({
composeId,
})
.then(() => {
toast.success("Compose Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Compose");
});
}).catch(() => {
toast.error("Error to deploy Compose");
});
await refetch();
})

View File

@@ -90,7 +90,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full relative z-[5]"
className="w-full relative z-[5]"
>
<div className="flex flex-col overflow-auto">
<FormField

View File

@@ -85,7 +85,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}

View File

@@ -50,17 +50,13 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
applicationStatus: "running",
})
.then(async () => {
toast.success("Database Deploying....");
toast.success("Deploying Database....");
await refetch();
await deploy({
mariadbId,
})
.then(() => {
toast.success("Database Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Database");
});
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {

View File

@@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
@@ -136,7 +137,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-3">
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
<Label>External Host</Label>
<Input disabled value={connectionUrl} />
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
interface Props {
mariadbId: string;
@@ -29,20 +30,18 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<Input
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
type="password"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Root Password</Label>
<div className="flex flex-row gap-4">
<Input
<ToggleVisibilityInput
disabled
value={data?.databaseRootPassword}
type="password"
/>
</div>
</div>
@@ -58,7 +57,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<Input
<ToggleVisibilityInput
disabled
value={`mariadb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
/>

View File

@@ -85,7 +85,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}

View File

@@ -50,17 +50,13 @@ export const DeployMongo = ({ mongoId }: Props) => {
applicationStatus: "running",
})
.then(async () => {
toast.success("Database Deploying....");
toast.success("Deploying Database....");
await refetch();
await deploy({
mongoId,
})
.then(() => {
toast.success("Database Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Database");
});
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {

View File

@@ -1,3 +1,4 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -136,7 +137,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<Input disabled value={connectionUrl} />
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
interface Props {
mongoId: string;
@@ -26,10 +27,9 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<Input
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
type="password"
/>
</div>
</div>
@@ -46,7 +46,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<Input
<ToggleVisibilityInput
disabled
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
/>

View File

@@ -85,7 +85,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}

View File

@@ -50,17 +50,13 @@ export const DeployMysql = ({ mysqlId }: Props) => {
applicationStatus: "running",
})
.then(async () => {
toast.success("Database Deploying....");
toast.success("Deploying Database....");
await refetch();
await deploy({
mysqlId,
})
.then(() => {
toast.success("Database Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Database");
});
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {

View File

@@ -1,3 +1,4 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -136,7 +137,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<Input disabled value={connectionUrl} />
<ToggleVisibilityInput disabled value={connectionUrl} />
</div>
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
interface Props {
mysqlId: string;
@@ -29,20 +30,18 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<Input
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
type="password"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Root Password</Label>
<div className="flex flex-row gap-4">
<Input
<ToggleVisibilityInput
disabled
value={data?.databaseRootPassword}
type="password"
/>
</div>
</div>
@@ -58,7 +57,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<Input
<ToggleVisibilityInput
disabled
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
/>

View File

@@ -85,7 +85,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}

View File

@@ -50,17 +50,13 @@ export const DeployPostgres = ({ postgresId }: Props) => {
applicationStatus: "running",
})
.then(async () => {
toast.success("Database Deploying....");
toast.success("Deploying Database....");
await refetch();
await deploy({
postgresId,
})
.then(() => {
toast.success("Database Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Database");
});
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {

View File

@@ -1,3 +1,4 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -137,7 +138,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<Input disabled value={connectionUrl} />
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
interface Props {
postgresId: string;
@@ -29,10 +30,9 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<Input
disabled
<ToggleVisibilityInput
value={data?.databasePassword}
type="password"
disabled
/>
</div>
</div>
@@ -48,7 +48,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Internal Connection URL </Label>
<Input
<ToggleVisibilityInput
disabled
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
/>

View File

@@ -22,16 +22,26 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder } from "lucide-react";
import { useEffect } from "react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug";
const AddTemplateSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
appName: z
.string()
.min(1, {
message: "App name is required",
})
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
message:
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
});
@@ -39,10 +49,13 @@ type AddTemplate = z.infer<typeof AddTemplateSchema>;
interface Props {
projectId: string;
projectName?: string;
}
export const AddApplication = ({ projectId }: Props) => {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation();
@@ -50,34 +63,34 @@ export const AddApplication = ({ projectId }: Props) => {
const form = useForm<AddTemplate>({
defaultValues: {
name: "",
appName: `${slug}-`,
description: "",
},
resolver: zodResolver(AddTemplateSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddTemplate) => {
await mutateAsync({
name: data.name,
appName: data.appName,
description: data.description,
projectId,
})
.then(async () => {
toast.success("Service Created");
form.reset();
setVisible(false);
await utils.project.one.invalidate({
projectId,
});
})
.catch(() => {
.catch((e) => {
toast.error("Error to create the service");
});
};
return (
<Dialog>
<Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
@@ -95,29 +108,46 @@ export const AddApplication = ({ projectId }: Props) => {
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Frontend" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Frontend"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
field.onChange(val);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>AppName</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
@@ -136,47 +166,6 @@ export const AddApplication = ({ projectId }: Props) => {
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="buildType"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Build Type</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dockerfile" />
</FormControl>
<FormLabel className="font-normal">
Dockerfile
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="nixpacks" />
</FormControl>
<FormLabel className="font-normal">Nixpacks</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="heroku_buildpacks" />
</FormControl>
<FormLabel className="font-normal">
Heroku Buildpacks
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
</form>
<DialogFooter>

View File

@@ -34,12 +34,22 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { slugify } from "@/lib/slug";
const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(),
name: z.string().min(1, {
message: "Name is required",
}),
appName: z
.string()
.min(1, {
message: "App name is required",
})
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
message:
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
description: z.string().optional(),
});
@@ -47,11 +57,12 @@ type AddCompose = z.infer<typeof AddComposeSchema>;
interface Props {
projectId: string;
projectName?: string;
}
export const AddCompose = ({ projectId }: Props) => {
export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const slug = slugify(projectName);
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -60,6 +71,7 @@ export const AddCompose = ({ projectId }: Props) => {
name: "",
description: "",
composeType: "docker-compose",
appName: `${slug}-`,
},
resolver: zodResolver(AddComposeSchema),
});
@@ -74,6 +86,7 @@ export const AddCompose = ({ projectId }: Props) => {
description: data.description,
projectId,
composeType: data.composeType,
appName: data.appName,
})
.then(async () => {
toast.success("Compose Created");
@@ -120,14 +133,34 @@ export const AddCompose = ({ projectId }: Props) => {
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Frontend" {...field} />
<Input
placeholder="Frontend"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
field.onChange(val);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>AppName</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="composeType"

View File

@@ -27,10 +27,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Database } from "lucide-react";
import { useEffect } from "react";
import { Database, AlertTriangle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -58,6 +59,15 @@ const databasesUserDefaultPlaceholder: Record<
const baseDatabaseSchema = z.object({
name: z.string().min(1, "Name required"),
appName: z
.string()
.min(1, {
message: "App name is required",
})
.regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, {
message:
"App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'",
}),
databasePassword: z.string(),
dockerImage: z.string(),
description: z.string().nullable(),
@@ -101,30 +111,52 @@ const mySchema = z.discriminatedUnion("type", [
.merge(baseDatabaseSchema),
]);
const databasesMap = {
postgres: {
icon: <PostgresqlIcon />,
label: "PostgreSQL",
},
mongo: {
icon: <MongodbIcon />,
label: "MongoDB",
},
mariadb: {
icon: <MariadbIcon />,
label: "MariaDB",
},
mysql: {
icon: <MysqlIcon />,
label: "MySQL",
},
redis: {
icon: <RedisIcon />,
label: "Redis",
},
};
type AddDatabase = z.infer<typeof mySchema>;
interface Props {
projectId: string;
projectName?: string;
}
export const AddDatabase = ({ projectId }: Props) => {
export const AddDatabase = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const { mutateAsync: createPostgresql } = api.postgres.create.useMutation();
const { mutateAsync: createMongo } = api.mongo.create.useMutation();
const { mutateAsync: createRedis } = api.redis.create.useMutation();
const { mutateAsync: createMariadb } = api.mariadb.create.useMutation();
const { mutateAsync: createMysql } = api.mysql.create.useMutation();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const redisMutation = api.redis.create.useMutation();
const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation();
const form = useForm<AddDatabase>({
defaultValues: {
type: "postgres",
dockerImage: "",
name: "",
appName: `${slug}-`,
databasePassword: "",
description: "",
databaseName: "",
@@ -133,76 +165,65 @@ export const AddDatabase = ({ projectId }: Props) => {
resolver: zodResolver(mySchema),
});
const type = form.watch("type");
useEffect(() => {
form.reset({
type: "postgres",
dockerImage: "",
name: "",
databasePassword: "",
description: "",
databaseName: "",
databaseUser: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const activeMutation = {
postgres: postgresMutation,
mongo: mongoMutation,
redis: redisMutation,
mariadb: mariadbMutation,
mysql: mysqlMutation,
};
const onSubmit = async (data: AddDatabase) => {
const defaultDockerImage =
data.dockerImage || dockerImageDefaultPlaceholder[data.type];
let promise: Promise<unknown> | null = null;
const commonParams = {
name: data.name,
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
description: data.description,
};
if (data.type === "postgres") {
promise = createPostgresql({
name: data.name,
dockerImage: defaultDockerImage,
promise = postgresMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
description: data.description,
});
} else if (data.type === "mongo") {
promise = createMongo({
name: data.name,
dockerImage: defaultDockerImage,
promise = mongoMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
description: data.description,
});
} else if (data.type === "redis") {
promise = createRedis({
name: data.name,
dockerImage: defaultDockerImage,
promise = redisMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
projectId,
description: data.description,
});
} else if (data.type === "mariadb") {
promise = createMariadb({
name: data.name,
dockerImage: defaultDockerImage,
promise = mariadbMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
projectId,
databaseRootPassword: data.databaseRootPassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
description: data.description,
});
} else if (data.type === "mysql") {
promise = createMysql({
name: data.name,
dockerImage: defaultDockerImage,
promise = mysqlMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
projectId,
databaseRootPassword: data.databaseRootPassword,
description: data.description,
});
}
@@ -210,6 +231,17 @@ export const AddDatabase = ({ projectId }: Props) => {
await promise
.then(async () => {
toast.success("Database Created");
form.reset({
type: "postgres",
dockerImage: "",
name: "",
appName: `${projectName}-`,
databasePassword: "",
description: "",
databaseName: "",
databaseUser: "",
});
setVisible(false);
await utils.project.one.invalidate({
projectId,
});
@@ -220,7 +252,7 @@ export const AddDatabase = ({ projectId }: Props) => {
}
};
return (
<Dialog>
<Dialog open={visible} onOpenChange={setVisible}>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
@@ -230,18 +262,10 @@ export const AddDatabase = ({ projectId }: Props) => {
<span>Database</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen md:max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Databases</DialogTitle>
</DialogHeader>
{/* {isError && (
<div className="flex items-center flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)} */}
<Form {...form}>
<form
@@ -264,99 +288,40 @@ export const AddDatabase = ({ projectId }: Props) => {
defaultValue={field.value}
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
>
<FormItem className="flex w-full items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="postgres"
id="postgres"
className="peer sr-only"
/>
<Label
htmlFor="postgres"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<PostgresqlIcon />
Postgresql
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="mysql"
id="mysql"
className="peer sr-only"
/>
<Label
htmlFor="mysql"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MysqlIcon />
Mysql
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="mariadb"
id="mariadb"
className="peer sr-only"
/>
<Label
htmlFor="mariadb"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MariadbIcon />
Mariadb
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="mongo"
id="mongo"
className="peer sr-only"
/>
<Label
htmlFor="mongo"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<MongodbIcon />
Mongo
</Label>
</div>
</FormControl>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl className="w-full">
<div>
<RadioGroupItem
value="redis"
id="redis"
className="peer sr-only"
/>
<Label
htmlFor="redis"
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
>
<RedisIcon />
Redis
</Label>
</div>
</FormControl>
</FormItem>
{Object.entries(databasesMap).map(([key, value]) => (
<FormItem
key={key}
className="flex w-full items-center space-x-3 space-y-0"
>
<FormControl className="w-full">
<div>
<RadioGroupItem
value={key}
id={key}
className="peer sr-only"
/>
<Label
htmlFor={key}
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
>
{value.icon}
{value.label}
</Label>
</div>
</FormControl>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
{activeMutation[field.value].isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{activeMutation[field.value].error?.message}
</span>
</div>
)}
</FormItem>
)}
/>
@@ -372,13 +337,34 @@ export const AddDatabase = ({ projectId }: Props) => {
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
<Input
placeholder="Name"
{...field}
onChange={(e) => {
const val = e.target.value?.trim() || "";
form.setValue("appName", `${slug}-${val}`);
field.onChange(val);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>AppName</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}

View File

@@ -85,7 +85,7 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
className="w-full space-y-4"
>
<FormField
control={form.control}

View File

@@ -50,17 +50,13 @@ export const DeployRedis = ({ redisId }: Props) => {
applicationStatus: "running",
})
.then(async () => {
toast.success("Database Deploying....");
toast.success("Deploying Database...");
await refetch();
await deploy({
redisId,
})
.then(() => {
toast.success("Database Deployed Succesfully");
})
.catch(() => {
toast.error("Error to deploy Database");
});
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {

View File

@@ -1,3 +1,4 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -129,7 +130,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<Input disabled value={connectionUrl} />
<ToggleVisibilityInput value={connectionUrl} disabled />
</div>
</div>
)}

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
interface Props {
redisId: string;
@@ -25,10 +26,9 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<Input
disabled
<ToggleVisibilityInput
value={data?.databasePassword}
type="password"
disabled
/>
</div>
</div>
@@ -44,7 +44,7 @@ export const ShowInternalRedisCredentials = ({ redisId }: Props) => {
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<Input
<ToggleVisibilityInput
disabled
value={`redis://default:${data?.databasePassword}@${data?.appName}:6379`}
/>

View 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>
);
};

View File

@@ -51,6 +51,9 @@ const randomImages = [
export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
const form = useForm<Profile>({
defaultValues: {
email: data?.email || "",

View File

@@ -32,7 +32,6 @@ export const ShowSettings = () => {
<ShowCertificates />
<WebDomain />
<WebServer />
<ShowUsers />
</>
)}

View File

@@ -38,6 +38,7 @@ const addPermissions = z.object({
canDeleteServices: z.boolean().optional().default(false),
canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
});
type AddPermissions = z.infer<typeof addPermissions>;
@@ -80,6 +81,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteServices: data.canDeleteServices,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
});
}
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
@@ -95,6 +97,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
})
.then(async () => {
toast.success("Permissions updated");
@@ -247,6 +250,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="canAccessToAPI"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Access to API/CLI</FormLabel>
<FormDescription>
Allow the user to access to the API/CLI
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="accesedProjects"

View File

@@ -194,7 +194,7 @@ export const WebServer = () => {
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>

View File

@@ -95,7 +95,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
<form
id="hook-form-update-main-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative"
className="w-full space-y-4 relative"
>
<div className="flex flex-col">
<FormField

View File

@@ -98,7 +98,7 @@ export const ShowServerMiddlewareConfig = ({ children }: Props) => {
<form
id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative overflow-auto"
className="w-full space-y-4 relative overflow-auto"
>
<div className="flex flex-col">
<FormField

View File

@@ -98,7 +98,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
<form
id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full py-4 relative overflow-auto"
className="w-full space-y-4 relative overflow-auto"
>
<div className="flex flex-col">
<FormField

View File

@@ -46,7 +46,7 @@ export const CodeEditor = ({
)}
/>
{props.disabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
<div className="absolute top-0 rounded-md left-0 w-full h-full flex items-center justify-center z-[10] [background:var(--overlay)]" />
)}
</div>
);

View 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>
);
};

View File

@@ -36,12 +36,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<div className="space-y-4 w-full">
{children}
</div>
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>

View File

@@ -0,0 +1 @@
ALTER TABLE "auth" ADD COLUMN "token" text;

View File

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

View File

@@ -0,0 +1 @@
ALTER TABLE "user" ALTER COLUMN "token" DROP NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -106,6 +106,27 @@
"when": 1716715367982,
"tag": "0014_same_hammerhead",
"breakpoints": true
},
{
"idx": 15,
"version": "6",
"when": 1717564517104,
"tag": "0015_fearless_callisto",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1719109196484,
"tag": "0016_chunky_leopardon",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1719109531147,
"tag": "0017_yummy_norrin_radd",
"breakpoints": true
}
]
}

15
lib/slug.ts Normal file
View 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,
});
};

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.2.0",
"version": "v0.2.4",
"private": true,
"license": "AGPL-3.0-only",
"type": "module",
@@ -31,11 +31,11 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"@codemirror/language":"^6.10.1",
"@aws-sdk/client-s3": "3.515.0",
"@codemirror/legacy-modes":"6.4.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@faker-js/faker": "^8.4.1",
"@hookform/resolvers": "^3.3.4",
"@lucia-auth/adapter-drizzle": "1.0.7",
@@ -76,6 +76,7 @@
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"copy-to-clipboard": "^3.3.3",
"copy-webpack-plugin": "^12.0.2",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dockerode-compose": "^1.4.0",
@@ -105,17 +106,18 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"recharts": "^2.12.3",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"tar-fs": "3.0.5",
"@dokploy/trpc-openapi": "0.0.4",
"use-resize-observer": "9.1.0",
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"copy-webpack-plugin": "^12.0.2"
"zod": "^3.23.4"
},
"devDependencies": {
"@biomejs/biome": "1.7.1",
@@ -129,6 +131,7 @@
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/swagger-ui-react": "^4.18.3",
"@types/tar-fs": "2.0.4",
"@types/ws": "8.5.10",
"autoprefixer": "^10.4.14",

View File

@@ -5,14 +5,14 @@ import { createTRPCContext } from "@/server/api/trpc";
// export API handler
export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError:
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
router: appRouter,
createContext: createTRPCContext,
onError:
process.env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});

View File

@@ -210,9 +210,12 @@ const Project = (
Actions
</DropdownMenuLabel>
<DropdownMenuSeparator />
<AddApplication projectId={projectId} />
<AddDatabase projectId={projectId} />
<AddCompose projectId={projectId} />
<AddApplication
projectId={projectId}
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
<AddCompose projectId={projectId} projectName={data?.name} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1,14 +1,26 @@
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
const Page = () => {
const { data } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: data?.id || "",
},
{
enabled: !!data?.id && data?.rol === "user",
},
);
return (
<div className="flex flex-col gap-4 w-full">
<ProfileForm />
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
</div>
);
};

63
pages/swagger.tsx Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ import { dockerRouter } from "./routers/docker";
import { composeRouter } from "./routers/compose";
import { registryRouter } from "./routers/registry";
import { clusterRouter } from "./routers/cluster";
/**
* This is the primary router for your server.
*
@@ -39,6 +40,7 @@ export const appRouter = createTRPCRouter({
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
@@ -50,7 +52,6 @@ export const appRouter = createTRPCRouter({
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
compose: composeRouter,
registry: registryRouter,
cluster: clusterRouter,
});

View File

@@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
}
} catch (error) {
} catch (error: unknown) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the application",

View File

@@ -26,6 +26,7 @@ import {
updateAuthById,
verify2FA,
} from "../services/auth";
import { luciaToken } from "@/server/auth/token";
export const authRouter = createTRPCRouter({
createAdmin: publicProcedure
@@ -138,6 +139,23 @@ export const authRouter = createTRPCRouter({
return auth;
}),
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
const auth = await findAuthById(ctx.user.authId);
if (auth.token) {
await luciaToken.invalidateSession(auth.token);
}
const session = await luciaToken.createSession(auth?.id || "", {
expiresIn: 60 * 60 * 24 * 30,
});
await updateAuthById(auth.id, {
token: session.id,
});
return auth;
}),
one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => {
const auth = await findAuthById(input.id);
return auth;
@@ -196,4 +214,7 @@ export const authRouter = createTRPCRouter({
});
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
});

View File

@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
import { removeDeploymentsByComposeId } from "../services/deployment";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { createCommand } from "@/server/utils/builders/compose";
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
import {
generatePassword,
loadTemplateModule,
readComposeFile,
} from "@/templates/utils";
import { findAdmin } from "../services/admin";
import { TRPCError } from "@trpc/server";
import { findProjectById, slugifyProjectName } from "../services/project";
import { findProjectById } from "../services/project";
import { createMount } from "../services/mount";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { templates } from "@/templates/templates";
import { slugify } from "@/lib/slug";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
const project = await findProjectById(input.projectId);
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts } = generate({
serverIp: admin.serverIp,
projectName: projectName,
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
env: envs.join("\n"),
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
});
if (ctx.user.rol === "user") {

View File

@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mariadb database",

View File

@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mongo database",

View File

@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mysql database",

View File

@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting postgresql database",

View File

@@ -1,4 +1,8 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
cliProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateProject,
@@ -54,6 +58,7 @@ export const projectRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {

View File

@@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { manageRegistry } from "@/server/utils/traefik/registry";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { docker } from "@/server/constants";
import { execAsync } from "@/server/utils/process/execAsync";
export const registryRouter = createTRPCRouter({
create: adminProcedure
@@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
.input(apiTestRegistry)
.mutation(async ({ input }) => {
try {
const result = await docker.checkAuth({
username: input.username,
password: input.password,
serveraddress: input.registryUrl,
});
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
return true;
} catch (error) {
console.log(error);
console.log("Error Registry:", error);
return false;
}
}),

View File

@@ -41,6 +41,8 @@ import {
} from "../services/settings";
import { canAccessToTraefikFiles } from "../services/user";
import { recreateDirectory } from "@/server/utils/filesystem/directory";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { appRouter } from "../root";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
@@ -242,5 +244,50 @@ export const settingsRouter = createTRPCRouter({
}
return readConfigInPath(input.path);
}),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {
const protocol = ctx.req.headers["x-forwarded-proto"];
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`;
const openApiDocument = generateOpenApiDocument(appRouter, {
title: "tRPC OpenAPI",
version: "1.0.0",
baseUrl: url,
docsUrl: `${url}/settings.getOpenApiDocument`,
tags: [
"admin",
"docker",
"compose",
"registry",
"cluster",
"user",
"domain",
"destination",
"backup",
"deployment",
"mounts",
"certificates",
"settings",
"security",
"redirects",
"port",
"project",
"application",
"mysql",
"postgres",
"redis",
"mongo",
"mariadb",
],
});
openApiDocument.info = {
title: "Dokploy API",
description: "Endpoints for dokploy",
version: getDokployVersion(),
};
return openApiDocument;
},
),
});
// apt-get install apache2-utils

View File

@@ -15,11 +15,27 @@ import { findAdmin } from "./admin";
import { createTraefikConfig } from "@/server/utils/traefik/application";
import { docker } from "@/server/constants";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
input: typeof apiCreateApplication._type,
) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Application with this 'AppName' already exists",
});
}
}
return await db.transaction(async (tx) => {
const newApplication = await tx
.insert(applications)

View File

@@ -13,10 +13,25 @@ import { join } from "node:path";
import { COMPOSE_PATH } from "@/server/constants";
import { cloneGithubRepository } from "@/server/utils/providers/github";
import { cloneGitRepository } from "@/server/utils/providers/git";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Compose = typeof compose.$inferSelect;
export const createCompose = async (input: typeof apiCreateCompose._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newDestination = await db
.insert(compose)
.values({
@@ -39,6 +54,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
export const createComposeByTemplate = async (
input: typeof compose.$inferInsert,
) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newDestination = await db
.insert(compose)
.values({

View File

@@ -46,9 +46,7 @@ export const getContainers = async () => {
.filter((container) => !container.name.includes("dokploy"));
return containers;
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
};
export const getConfig = async (containerId: string) => {
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
const config = JSON.parse(stdout);
return config;
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
};
export const getContainersByAppNameMatch = async (appName: string) => {
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
});
return containers || [];
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
return [];
};
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
});
return containers || [];
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
return [];
};

View File

@@ -5,10 +5,26 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMariadb = await db
.insert(mariadb)
.values({

View File

@@ -5,10 +5,26 @@ import { buildMongo } from "@/server/utils/databases/mongo";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMongo = await db
.insert(mongo)
.values({

View File

@@ -5,11 +5,27 @@ import { buildMysql } from "@/server/utils/databases/mysql";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { nanoid } from "nanoid";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMysql = await db
.insert(mysql)
.values({

View File

@@ -5,10 +5,26 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generatePassword } from "@/templates/utils";
import { generateAppName } from "@/server/db/schema/utils";
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newPostgres = await db
.insert(postgres)
.values({

View File

@@ -1,5 +1,14 @@
import { db } from "@/server/db";
import { type apiCreateProject, projects } from "@/server/db/schema";
import {
type apiCreateProject,
applications,
mariadb,
mongo,
mysql,
postgres,
projects,
redis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { findAdmin } from "./admin";
@@ -75,12 +84,40 @@ export const updateProjectById = async (
return result;
};
export const slugifyProjectName = (projectName: string): string => {
return projectName
.toLowerCase()
.replace(/[0-9]/g, "")
.replace(/[^a-z\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
export const validUniqueServerAppName = async (appName: string) => {
const query = await db.query.projects.findMany({
with: {
applications: {
where: eq(applications.appName, appName),
},
mariadb: {
where: eq(mariadb.appName, appName),
},
mongo: {
where: eq(mongo.appName, appName),
},
mysql: {
where: eq(mysql.appName, appName),
},
postgres: {
where: eq(postgres.appName, appName),
},
redis: {
where: eq(redis.appName, appName),
},
},
});
// Filter out items with non-empty fields
const nonEmptyProjects = query.filter(
(project) =>
project.applications.length > 0 ||
project.mariadb.length > 0 ||
project.mongo.length > 0 ||
project.mysql.length > 0 ||
project.postgres.length > 0 ||
project.redis.length > 0,
);
return nonEmptyProjects.length === 0;
};

View File

@@ -5,11 +5,27 @@ import { buildRedis } from "@/server/utils/databases/redis";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { generateAppName } from "@/server/db/schema/utils";
import { generatePassword } from "@/templates/utils";
export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => {
input.appName =
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newRedis = await db
.insert(redis)
.values({

View File

@@ -9,28 +9,35 @@ import {
} from "@/server/utils/traefik/registry";
import { removeService } from "@/server/utils/docker/utils";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { execAsync } from "@/server/utils/process/execAsync";
export type Registry = typeof registry.$inferSelect;
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
const admin = await findAdmin();
const newRegistry = await db
.insert(registry)
.values({
...input,
adminId: admin.adminId,
})
.returning()
.then((value) => value[0]);
return await db.transaction(async (tx) => {
const newRegistry = await tx
.insert(registry)
.values({
...input,
adminId: admin.adminId,
})
.returning()
.then((value) => value[0]);
if (!newRegistry) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting registry",
});
}
return newRegistry;
if (!newRegistry) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting registry",
});
}
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
return newRegistry;
});
};
export const removeRegistry = async (registryId: string) => {
@@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
await removeService("dokploy-registry");
}
await execAsync(`docker logout ${response.registryUrl}`);
return response;
} catch (error) {
throw new TRPCError({

View File

@@ -15,6 +15,8 @@ import superjson from "superjson";
import { ZodError } from "zod";
import { validateRequest } from "../auth/auth";
import type { Session, User } from "lucia";
import { validateBearerToken } from "../auth/token";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
/**
* 1. CONTEXT
@@ -59,9 +61,15 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
*/
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
const { session, user } = await validateRequest(req, res);
user;
let { session, user } = await validateBearerToken(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
return createInnerTRPCContext({
req,
res,
@@ -88,19 +96,22 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* errors on the backend.
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
const t = initTRPC
.meta<OpenApiMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
@@ -147,6 +158,20 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
});
});
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: ctx.session,
user: ctx.user,
// session: { ...ctx.session, user: ctx.user },
},
});
});
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session || !ctx.user || ctx.user.rol !== "admin") {
throw new TRPCError({ code: "UNAUTHORIZED" });

View File

@@ -33,14 +33,17 @@ declare module "lucia" {
}
}
export type ReturnValidateToken = Promise<{
user: (User & { authId: string }) | null;
session: Session | null;
}>;
export async function validateRequest(
req: IncomingMessage,
res: ServerResponse,
): Promise<{
user: (User & { authId: string }) | null;
session: Session | null;
}> {
): ReturnValidateToken {
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
if (!sessionId) {
return {
user: null,

48
server/auth/token.ts Normal file
View 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,
}),
};
};

View File

@@ -20,6 +20,7 @@ import {
} from "drizzle-orm/pg-core";
import { generateAppName } from "./utils";
import { registry } from "./registry";
import { generatePassword } from "@/templates/utils";
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
@@ -309,6 +310,7 @@ const createSchema = createInsertSchema(applications, {
export const apiCreateApplication = createSchema.pick({
name: true,
appName: true,
description: true,
projectId: true,
});

View File

@@ -43,6 +43,7 @@ export const auth = pgTable("auth", {
rol: roles("rol").notNull(),
image: text("image").$defaultFn(() => generateRandomImage()),
secret: text("secret"),
token: text("token"),
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
createdAt: text("createdAt")
.notNull()

View File

@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { applicationStatus } from "./shared";
import { mounts } from "./mount";
import { generatePassword } from "@/templates/utils";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git",
@@ -79,6 +80,7 @@ export const apiCreateCompose = createSchema.pick({
description: true,
projectId: true,
composeType: true,
appName: true,
});
export const apiCreateComposeByTemplate = createSchema

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId")
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
export const apiCreateMariaDB = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
databaseRootPassword: true,
projectId: true,

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mongo = pgTable("mongo", {
mongoId: text("mongoId")
@@ -73,6 +74,7 @@ const createSchema = createInsertSchema(mongo, {
export const apiCreateMongo = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
projectId: true,
description: true,

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId")
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
export const apiCreateMySql = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
projectId: true,
description: true,

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const postgres = pgTable("postgres", {
postgresId: text("postgresId")
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
export const apiCreatePostgres = createSchema
.pick({
name: true,
appName: true,
databaseName: true,
databaseUser: true,
databasePassword: true,

View File

@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { projects } from "./project";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const redis = pgTable("redis", {
redisId: text("redisId")
@@ -69,12 +70,12 @@ const createSchema = createInsertSchema(redis, {
export const apiCreateRedis = createSchema
.pick({
name: true,
appName: true,
databasePassword: true,
dockerImage: true,
projectId: true,
description: true,
})
.required();
export const apiFindOneRedis = createSchema

View File

@@ -1,13 +1,6 @@
import { auth } from "./auth";
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
// export const sessionTable = sqliteTable("session", {
// id: text("id").notNull().primaryKey(),
// userId: text("user_id")
// .notNull()
// .references(() => users.id),
// expiresAt: integer("expires_at").notNull(),
// });
export const sessionTable = pgTable("session", {
id: text("id").primaryKey(),
userId: text("user_id")

View File

@@ -32,6 +32,7 @@ export const users = pgTable("user", {
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
@@ -105,6 +106,7 @@ export const apiAssignPermissions = createSchema
accesedServices: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
})
.required();

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