Compare commits

..
96 Commits
Author SHA1 Message Date
charlesgauthereau 8ce3988613 Ready for 1.1.2-rc.3 2025-09-26 09:15:22 +02:00
charlesgauthereau b7f161d200 Ready for 1.1.2-rc.3 2025-09-26 09:04:18 +02:00
charlesgauthereau a3f5c4e494 Ready for 1.1.2-rc.2 2025-09-23 21:19:11 +02:00
charlesgauthereau f74f378c0b Ready for 1.1.2-rc.1 2025-09-23 20:50:20 +02:00
charlesgauthereau 385836d5fe Ready for 1.1.2-rc.1 2025-09-23 20:35:48 +02:00
charlesgauthereau 461e3ff7c9 Ready for 1.1.2-rc.1 2025-09-23 19:55:07 +02:00
charlesgauthereau 54a233b207 Ready for 1.1.2-rc.1 2025-09-23 19:50:33 +02:00
charlesgauthereau 2d2453acf7 Refactoring on some files 2025-09-23 19:42:38 +02:00
charlesgauthereau 654ff6b9a5 Finish working on backups side deletion feature, to be review, test and deply in RC for tests. 2025-09-22 20:31:29 +02:00
charlesgauthereau 34ff11f377 Working on table data in detabase backups. 2025-09-19 13:28:40 +02:00
charlesgauthereau b95e145a8b Working on retention policy and cron system. 2025-09-19 09:58:54 +02:00
charlesgauthereau bce6c9d55f Working on retention policy and cron system. 2025-09-18 09:47:08 +02:00
charlesgauthereau c3e2978919 Working on retention policy and cron system. 2025-09-14 16:46:23 +02:00
charlesgauthereau 2d559e456d Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt. 2025-08-31 18:24:07 +02:00
charlesgauthereau 45e3b31fda Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt. 2025-08-29 15:31:23 +02:00
charlesgauthereau c8da2579e8 Working on the retention system. 2025-08-28 10:01:17 +02:00
charlesgauthereau c7e30e1659 Working on the retention system. 2025-08-28 09:59:32 +02:00
charlesgauthereau 7be23876aa Working on the retention system. 2025-08-28 09:56:10 +02:00
charlesgauthereau 98f1b329ae Working on the retention system. 2025-08-28 09:53:32 +02:00
charlesgauthereau 19ea5543ea Working on the retention system. 2025-08-28 09:47:42 +02:00
charlesgauthereau 010396ba4d Working on the retention system. 2025-08-28 09:45:47 +02:00
charlesgauthereau f055744432 Working on the retention system. 2025-08-28 09:45:32 +02:00
charlesgauthereau 1920b29a55 Working on the retention system. 2025-08-28 09:45:22 +02:00
charlesgauthereau a0c8d1fb68 Working on the retention system. 2025-08-28 09:34:02 +02:00
charlesgauthereau 59dc3b71b8 Working on the retention system. 2025-08-28 09:32:30 +02:00
charlesgauthereau 598eb44952 Working on the retention system. 2025-08-28 09:31:57 +02:00
charlesgauthereau ac49047177 Working on the retention system. 2025-08-28 09:27:47 +02:00
charlesgauthereau 4448c9e166 Working on the retention system. 2025-08-28 09:23:04 +02:00
charlesgauthereau d70778b556 Working on the retention system. 2025-08-28 09:17:57 +02:00
charlesgauthereau 66edb77113 Working on the retention system. 2025-08-28 09:16:54 +02:00
charlesgauthereau 0f7b3eae11 Working on the retention system. 2025-08-27 21:38:58 +02:00
charlesgauthereau 2f9e33f4db Patching a bug when delete an organization, add refetch. 2025-08-27 21:16:13 +02:00
charlesgauthereau dd7a65262c Patching a bug when delete an organization, add refetch. 2025-08-27 18:07:10 +02:00
charlesgauthereau 4c0d2b1f7f Adding new version of the ButtonWithConfirm component. 2025-08-27 18:01:28 +02:00
charlesgauthereau 60728885b6 Working on the retention system. 2025-08-24 20:11:34 +02:00
charlesgauthereau 7ac99a6618 Working on the organization and permissions. 2025-08-24 19:34:18 +02:00
charlesgauthereau 4ed91c1297 Working on the organization and permissions. 2025-08-24 19:33:42 +02:00
charlesgauthereau 44be8009fd Bug on stats. 2025-08-07 16:23:06 +02:00
charlesgauthereau 96630a0ef7 Merge branch 'feature' 2025-08-07 15:46:52 +02:00
charlesgauthereau 8c9d694b40 Bug on init. 2025-08-07 15:46:41 +02:00
charlesgauthereau 1f09c1507a Merge branch 'feature'
# Conflicts:
#	src/features/dashboard/backup/columns.tsx
2025-08-07 15:06:05 +02:00
Soluce TechnologiesandGitHub 141241d555 Update README.md 2025-08-05 08:22:36 +02:00
Soluce TechnologiesandGitHub c605e8173b Update README.md 2025-08-05 08:21:18 +02:00
charles-gauthereau 37eb8f319f Merge remote-tracking branch 'origin/main' 2025-08-04 08:07:38 +02:00
charles-gauthereau 0492d244b3 columns.tsx 2025-08-04 08:07:26 +02:00
charlesgauthereau baeec9f7a0 Some fix on the s3 and local methods for the files saving, we have to improve testing. Working on table actions. 2025-08-03 12:17:10 +02:00
charlesgauthereau 449c9c2cfe Working on the s3 save dump feature. 2025-08-02 20:13:29 +02:00
charlesgauthereau d13e4b7d40 Working on statistics 2025-08-02 11:14:51 +02:00
charlesgauthereau 13241811d0 Placeholder empty state component. 2025-08-02 08:48:45 +02:00
Soluce TechnologiesandGitHub 4b52dc4a1e Update README.md 2025-08-01 09:35:51 +02:00
charlesgauthereau 63cb6cb7a3 Working on some routing errors. 2025-08-01 09:21:37 +02:00
charlesgauthereau 0511ad289c Merge branch 'main' into feature
# Conflicts:
#	README.md
2025-08-01 08:35:20 +02:00
charlesgauthereau 057a51499e Release 2025-08-01 08:35:02 +02:00
charlesgauthereau 1f01d9c372 Release 2025-08-01 08:34:17 +02:00
charlesgauthereau 0684a4887a Merge branch 'main' into feature
# Conflicts:
#	app/(customer)/dashboard/[slug]/statistics/page.tsx
2025-08-01 08:33:26 +02:00
charlesgauthereau 96be14e1ba Working on the s3 feature. 2025-07-31 17:58:40 +02:00
charlesgauthereau eec3c2737b Working on the s3 feature. 2025-07-31 17:32:03 +02:00
charlesgauthereau a6b960ffb2 Working on the s3 feature. 2025-07-29 09:53:38 +02:00
charlesgauthereau d3f465b5d4 Working on the home page. 2025-07-29 08:51:50 +02:00
charlesgauthereau 2139dfd55d For release. 2025-07-28 18:55:49 +02:00
charlesgauthereau 9f53181467 For release. 2025-07-28 18:43:31 +02:00
charlesgauthereau c96e13c72f For release. 2025-07-28 18:18:23 +02:00
charlesgauthereau 58d9179152 For release. 2025-07-28 17:58:58 +02:00
charlesgauthereau 17469696ee For release. 2025-07-28 16:53:21 +02:00
charlesgauthereau 85047e7509 Working on some bugs. 2025-07-27 21:26:31 +02:00
charlesgauthereau f9dc15ba73 Routing error. 2025-07-27 10:23:56 +02:00
charlesgauthereau 7be542f676 Some bug and visuals improvements. 2025-07-26 22:23:00 +02:00
charlesgauthereau 847a567c52 Working on bugs, add delete restore. Review the auth style. working on the RBAC. 2025-07-26 16:45:43 +02:00
charlesgauthereau 1ffbd6efc8 Working on bugs, add delete restore. Review the auth style. working on the RBAC. 2025-07-26 16:34:06 +02:00
charlesgauthereau f8f599e4dd Working on the Organization side. 2025-07-26 14:43:59 +02:00
charlesgauthereau f7cd169090 Working on header and CSP. 2025-07-25 22:06:40 +02:00
charlesgauthereau bb4003b0db Working on header and CSP. 2025-07-25 21:41:28 +02:00
charlesgauthereau 159398465f But on private and public dir permissions. 2025-07-18 11:31:24 +02:00
charlesgauthereau 76e5e55f14 Ready for release soon. 2025-07-17 13:22:56 +02:00
charlesgauthereau 605946abaa Ready for release soon. 2025-07-17 12:59:38 +02:00
charlesgauthereau 2890d6ac40 Ready for release soon. 2025-07-17 12:43:00 +02:00
charlesgauthereau ac3b4e424a Ready for release soon. 2025-07-17 12:05:39 +02:00
charlesgauthereau 8154f85eb6 Ready for release soon. 2025-07-17 12:01:11 +02:00
charlesgauthereau 45312207c9 Ready for release soon. 2025-07-17 11:58:13 +02:00
charlesgauthereau 128792645b Ready for release soon. 2025-07-17 11:50:27 +02:00
charlesgauthereau 4d52419247 Ready for release soon. 2025-07-17 09:28:57 +02:00
charlesgauthereau 7abdcd57f9 Ready for release soon. 2025-07-16 20:34:28 +02:00
charlesgauthereau 5e704ec18a Testing in local validated. 2025-07-16 20:16:18 +02:00
charlesgauthereau de9fe33194 Finish migration prisma to drizzle. 2025-07-16 18:15:28 +02:00
charlesgauthereau 385777535f Working on the organization system. 2025-07-16 12:36:11 +02:00
charlesgauthereau 405de7b323 Working on the Portabase debug. 2025-05-18 20:46:59 +02:00
Théo LAGACHE 6f2523e524 migration 2025-05-16 15:54:17 +02:00
charles-gauthereau 01019fe1a3 Patch force dynamic route for dashboard/agent 2025-02-21 08:59:37 +01:00
charles-gauthereau b1ecb7ce01 Bug. 2025-02-21 08:43:01 +01:00
charles-gauthereau 0cf350cdcc Bug. 2025-02-21 08:21:11 +01:00
charles-gauthereau ed5f7a43f2 Bug. 2025-02-20 10:26:37 +01:00
charles-gauthereau 54f0e87805 Deploy new version. 2025-02-20 09:26:38 +01:00
charles-gauthereau f570a67805 Deploy new version. 2025-02-14 09:29:47 +01:00
charles-gauthereau d0ec5902b5 Correcting some bugs in statistics. 2025-02-14 09:25:45 +01:00
charles-gauthereau 8586abf7e5 Correcting some bugs in statistics. 2025-02-13 19:20:55 +01:00
charles-gauthereau 6562718635 Correcting some bugs in statistics. 2025-01-12 17:53:39 +01:00
422 changed files with 25628 additions and 49221 deletions
+22 -1
View File
@@ -1,4 +1,25 @@
# Ignore Docker-related configs
.dockerignore
docker-compose.yml docker-compose.yml
README.md docker-compose*.yml
# Development/Editor config
.idea .idea
.vscode
# Node-related
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
pnpm-debug.log
# Next.js build output
.next .next
out
# Docs and markdowns
README.md
*.md
+33
View File
@@ -0,0 +1,33 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Type (s3, local)
STORAGE_TYPE=local
+22 -2
View File
@@ -29,6 +29,16 @@ jobs:
with: with:
images: solucetechnologies/portabase images: solucetechnologies/portabase
- name: Set tags
id: set-tags
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
TAGS="$TAGS,solucetechnologies/portabase:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image - name: Build and push Docker image
id: push id: push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -36,10 +46,20 @@ jobs:
context: . context: .
file: ./docker/dockerfile/Dockerfile file: ./docker/dockerfile/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.set-tags.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
target: prod target: prod
# Do not delete
# - name: Build and push Docker image
# id: push
# uses: docker/build-push-action@v6
# with:
# context: .
# file: ./docker/dockerfile/Dockerfile
# push: true
# tags: ${{ steps.meta.outputs.tags }}
# labels: ${{ steps.meta.outputs.labels }}
# target: prod
# - name: Generate artifact attestation # - name: Generate artifact attestation
# uses: actions/attest-build-provenance@v2 # uses: actions/attest-build-provenance@v2
+28
View File
@@ -0,0 +1,28 @@
name: Security Checks
on:
pull_request:
push:
branches: [ main ]
jobs:
sca-deps: # Dependency & container scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.20.0
with:
scan-type: 'fs'
format: 'table'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
secrets-gitleaks: # Secrets exposure
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for gitleaks
- uses: gitleaks/gitleaks-action@v2
with:
config-path: .gitleaks.toml # Optional, if you have a custom config
+1 -1
View File
@@ -4,7 +4,7 @@
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
.yarn/install-state.gz #.yarn/install-state.gz
# testing # testing
/coverage /coverage
+10
View File
@@ -0,0 +1,10 @@
title = "Custom gitleaks config"
[allowlist]
# Global allowlist patterns (wont be flagged)
regexes = [
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
]
paths = [
"src/utils/init.ts"
]
+5
View File
@@ -0,0 +1,5 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
+3
View File
@@ -0,0 +1,3 @@
{
"WillLuke.nextjs.hasPrompted": true
}
Binary file not shown.
-934
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,2 +1,2 @@
nodeLinker: "node-modules" nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.3.cjs
+4 -2
View File
@@ -1,2 +1,4 @@
migrate: CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
docker compose run --rm app sh -c "npx prisma migrate dev"
up:
@bash $(CLUSTER_SCRIPT)
+155 -115
View File
@@ -1,182 +1,222 @@
<!-- PROJECT LOGO --> <br />
<div align="center">
<a href="https://portabase.io">
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
</a>
[//]: # (<br />) <h3 align="center">Portabase</h3>
[//]: # (<div align="center">)
[//]: # ( <a href="https://github.com/othneildrew/Best-README-Template">)
[//]: # ( <img src="images/logo.png" alt="Logo" width="80" height="80">)
[//]: # ( </a>)
<h3 align="center">Portabase</h3>
<p align="center"> <p align="center">
Backup / Restore your databases instances Easily backup and restore your database instances with Portabase
<br /> <br />
<a href="https://portabase.io"><strong>Explore the docs »</strong></a> <a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
<br /> <br />
<br /> <br />
<a href="https://portabase.io">View Demo</a> <a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">View Demo</a>
· ·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> <a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
· ·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a> <a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43)
</p> </p>
</div>
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
## 📚 Table of Contents
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
- [Acknowledgments](#acknowledgments)
---
<!-- ABOUT THE PROJECT -->
## About The Project ## About The Project
Portabase is the server dashboard service working with portabase agents. **Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p> GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
### Built With ---
* [![NextJS][NextJS]][NextJS-url] ## 🚀 Getting Started
* [![Prisma][Prisma]][Prisma-url]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
### Prerequisites ### Prerequisites
Install docker on your machine Ensure Docker is installed on your machine before getting started.
### Installation ### Installation
Installation steps in order to use it locally and work on some features improvements To run Portabase locally:
1. Clone the repo 1. Clone the repository:
```sh ```bash
git clone https://github.com/Soluce-Technologies/portabase git clone https://github.com/Soluce-Technologies/portabase
``` cd portabase
3. Start the project ```
```sh 2. Start the development environment:
docker compose up ```bash
``` docker compose up
```
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
## 🛠️ Usage
<!-- ROADMAP --> Portabase provides a dashboard to manage database instances and backups. It supports:
## Roadmap
- [ ] Add Changelog - PostgreSQL (current)
- [ ] Improve documentation and repositories - MongoDB (coming soon)
- [ ] Add tests procedure - MySQL (coming soon)
- [ ] Add Release File
- [ ] Security improvements You can access the dashboard at `http://localhost:3000` after starting the project.
- [ ] Improve UX/UI
- [ ] Improve Workspace system ---
- [ ] Migrate to Drizzle ORM
- [ ] Multi locale support ## 🗺️ Roadmap
- [ ] Multi db support
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL - [x] PostgreSQL
- [ ] MongoDB - [ ] MongoDB
- [ ] MySQL - [ ] MySQL
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues). Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
<!-- CONTRIBUTING --> 1. Fork the repository
## Contributing 2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Give the project a ⭐ if you like it!
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". ### Top Contributors
Don't forget to give the project a star! Thanks again!
1. Fork the Project [![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
### Top contributors: ---
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors"> ## Developer Notes
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p> ### Environment Variables
```yml
# Environment
NODE_ENV=production
## Memo # Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format: # Project Info
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)``` # SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)``` # Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
For zenstask # S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
```zenstack generate``` # Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
```
<!-- LICENSE --> ### Semantic Versioning
## License
Distributed under the Apache License. See `LICENSE.txt` for more information. Use the following format for Docker image versioning:
<p align="right">(<a href="#readme-top">back to top</a>)</p> ```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
<!-- CONTACT --> ---
## Contact
Killian Larcher - killian.larcher@soluce-technologies.com ## 📄 License
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase) Distributed under the Apache License. See `LICENSE.txt` for more details.
<p align="right">(<a href="#readme-top">back to top</a>)</p> ---
<!-- MARKDOWN LINKS & IMAGES --> ## 📬 Contact
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
---
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white [NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Prisma]: https://img.shields.io/badge/Prisma-3982CE?style=for-the-badge&logo=Prisma&logoColor=white [Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
[Prisma-url]: https://www.prisma.io/ [ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
[NextJS-url]: https://nextjs.org/ [NextJS-url]: https://nextjs.org/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
+44 -24
View File
@@ -1,34 +1,54 @@
import React from "react"; "use client"
import React, {useEffect, useState} from "react";
import {LayoutAdmin} from "@/components/layout"; import {LayoutAdmin} from "@/components/layout";
import {currentUser} from "@/auth/current-user"; import Image from "next/image";
import {redirect} from "next/navigation"; import {env} from "@/env.mjs";
import {prisma} from "@/prisma"; import {useTheme} from "next-themes";
import {useSession} from "@/lib/auth/auth-client";
import {useRouter} from "next/navigation";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser() export default function Layout({children}: { children: React.ReactNode }) {
if(user){
const userInfo = await prisma.user.findUnique({ const { resolvedTheme } = useTheme();
where: { const [mounted, setMounted] = useState(false);
email: user?.email const router = useRouter();
} const { data: session } = useSession();
})
if(userInfo && userInfo.authMethod){ if (session && session.user && !session.user.banned && session.user.role !== "pending") {
redirect('/') router.replace("/dashboard/home");
// redirect('/dashboard')
}
} }
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
return ( return (
<LayoutAdmin> <div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
<div <div className="mx-auto w-full max-w-md">
className="w-full h-full grid " <div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
> <img
<div className="flex items-center justify-center py-12"> className="p-12 text-black dark:text-white"
{children} src={imageTheme}
// loading="eager"
alt="Logo"
// width={1024}
// height={1024}
/>
<span
className="text-sm text-muted-foreground -ml-12 -mb-12">
v{env.NEXT_PUBLIC_PROJECT_VERSION}
</span>
</div> </div>
<div>{children}</div>
</div> </div>
</LayoutAdmin> <footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
Powered by <span className="font-medium">Soluce Technologies</span>
</footer>
</div>
) )
} }
+13 -10
View File
@@ -1,28 +1,31 @@
"use client" "use client"
import {useEffect, useRef} from "react"; import {useEffect, useRef, useState} from "react";
import {useSearchParams} from "next/navigation"; import {useSearchParams} from "next/navigation";
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm"; import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
import {toast} from "sonner"; import {toast} from "sonner";
export default function SignInPage(props: { export default function SignInPage(props: {
searchParams: Promise<{ callbackUrl: string | undefined }> searchParams: Promise<{ callbackUrl: string | undefined }>
}) { }) {
const searchParams = useSearchParams(); const [urlParams, setUrlParams] = useState<URLSearchParams>();
const error = searchParams.get("error");
const isFirstRender = useRef(true);
useEffect(() => { useEffect(() => {
if (isFirstRender.current && error) { const urlParams = new URLSearchParams(window.location.search);
toast.error("Error occurred."); setUrlParams(urlParams);
isFirstRender.current = false; const error = urlParams.get("error");
console.log(urlParams.get("redirect"));
if (error?.includes("pending")) {
toast.error("Your account is not active.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
} }
}, [error]); }, []);
return ( return (
<div className="mx-auto grid w-[350px] gap-6"> <div className="mx-auto grid w-full gap-6">
<LoginForm/> <LoginForm/>
</div> </div>
) )
+2 -2
View File
@@ -1,9 +1,9 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {RegisterForm} from "@/components/wrappers/auth/Register/register-form/RegisterForm"; import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
return ( return (
<div className="mx-auto grid w-[350px] gap-6"> <div className="mx-auto grid w-full gap-6">
<RegisterForm/> <RegisterForm/>
</div> </div>
) )
+14 -25
View File
@@ -1,42 +1,31 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs"; import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const users = await db.query.user.findMany({
const user = await requiredCurrentUser() where: (fields) => isNull(fields.deletedAt),
with: {
const users = await prisma.user.findMany({ accounts: true
where: {
// id: {
// not: user.id
// },
deleted: {not: true},
} }
}) });
const settings = await prisma.settings.findUnique({ const settings = await db.query.setting.findFirst({
where: { where: (fields, {eq}) => eq(fields.name, "system"),
name: "system" });
}
})
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Administration Panel</PageTitle>
Administration Panel
</PageTitle>
</PageHeader> </PageHeader>
<PageContent className="mt-10"> <PageContent>
<AdminTabs settings={settings} currentUser={user} users={users}/> <AdminTabs settings={settings!} users={users}/>
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,19 +1,18 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import {prisma} from "@/prisma"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
export default async function RoutePage(props: PageParams<{ const agent = await db.query.agent.findFirst({
agentId: string; where: (fields, { eq }) => eq(fields.id, agentId),
}>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
}
}); });
if (!agent) { if (!agent) {
@@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Edit {agent.name}</PageTitle>
Edit {agent.name}
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<AgentForm defaultValues={agent} agentId={agent.id}/> <AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,92 +1,84 @@
import Link from "next/link"; import Link from "next/link";
import {GearIcon} from "@radix-ui/react-icons"; import {GearIcon} from "@radix-ui/react-icons";
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {formatDateLastContact} from "@/utils/date-formatting"; import {formatDateLastContact} from "@/utils/date-formatting";
import {buttonVariants} from "@/components/ui/button"; import {buttonVariants} from "@/components/ui/button";
import {Card, CardContent, CardHeader} from "@/components/ui/card"; import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard"; import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey"; import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, eq} from "drizzle-orm";
import {notFound} from "next/navigation";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
import {capitalizeFirstLetter} from "@/utils/text";
import {Server} from "lucide-react";
export default async function RoutePage(props: PageParams<{ agentId: string }>) { export default async function RoutePage(props: PageParams<{ agentId: string }>) {
const {agentId} = await props.params const {agentId} = await props.params
const agent = await prisma.agent.findUnique({ const agent = await db.query.agent.findFirst({
where: { where: eq(drizzleDb.schemas.agent.id, agentId),
id: agentId, with: {
}, databases: true
include: {
databases: {}
} }
}) })
const databaseId = 'db-123';
const totalBackups = await prisma.backup.count({ if (!agent) {
where: { notFound()
databaseId: databaseId, }
},
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
{agent.name} {capitalizeFirstLetter(agent.name)}
<Link className={buttonVariants({variant: "outline"})} <Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/agents/${agent.id}/edit`}> href={`/dashboard/agents/${agent.id}/edit`}>
<GearIcon className="w-7 h-7"/> <GearIcon className="w-7 h-7"/>
</Link> </Link>
{/*<AgentModalKey agent={agent}>*/}
{/* <Button variant="outline">*/}
{/* <KeyRound/>*/}
{/* </Button>*/}
{/*</AgentModalKey>*/}
</PageTitle> </PageTitle>
<PageActions className="justify-between">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
</PageActions>
</div> </div>
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription> <PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6"> <div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
<Card className="w-full sm:w-auto flex-1"> <Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
Backups <CardTitle className="text-sm font-medium">Databases</CardTitle>
</CardHeader> <Server className="h-4 w-4 text-muted-foreground" />
<CardContent></CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Success rate
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{successRate ?? "Unavailable for now."} <div className="text-2xl font-bold">{agent.databases.length}</div>
<p className="text-xs text-muted-foreground">Databases linked to this agent</p>
</CardContent> </CardContent>
</Card> </Card>
<Card className="w-full sm:w-auto flex-1"> <Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
Last contact <CardTitle className="text-sm font-medium">Last contact</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{formatDateLastContact(agent.lastContact)} <div className="text-2xl font-bold">{formatDateLastContact(agent.lastContact)}</div>
<p className="text-xs text-muted-foreground">Last contact with agent</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<Card className="w-full sm:w-auto flex-1"> <Card className="w-full sm:w-auto flex-1 mb-4">
<CardHeader className="font-bold text-xl"> <CardHeader className="font-bold text-xl">
Edge Key Edge Key
</CardHeader> </CardHeader>
@@ -94,7 +86,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
<AgentCardKey agent={agent}/> <AgentCardKey agent={agent}/>
</CardContent> </CardContent>
</Card> </Card>
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/> <CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
</PageContent> </PageContent>
</Page> </Page>
) )
@@ -1,28 +1,16 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import {currentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if(user.role != "admin"){
notFound()
}
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Create new agent</PageTitle>
Create new agent
</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<AgentForm/> <AgentForm />
</PageContent> </PageContent>
</Page> </Page>
) );
} }
@@ -1,22 +1,34 @@
import {PageParams} from "@/types/next"; import { PageParams } from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard"; import { AgentCard } from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from 'next/link' import Link from "next/link";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import {prisma} from "@/prisma"; import { notFound } from "next/navigation";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, eq, not} from "drizzle-orm";
import {Plus} from "lucide-react";
import {cn} from "@/lib/utils";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
export const dynamic = "force-dynamic";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const agents = await prisma.agent.findMany() const agents = await db.query.agent.findMany({
where: not(eq(drizzleDb.schemas.agent.isArchived, true))
});
if (!agents) {
notFound();
}
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle> <PageTitle>Agents</PageTitle>
Agents
</PageTitle>
{agents.length > 0 && ( {agents.length > 0 && (
<PageActions> <PageActions>
<Link href={"/dashboard/agents/new"}> <Link href={"/dashboard/agents/new"}>
@@ -25,22 +37,16 @@ export default async function RoutePage(props: PageParams<{}>) {
</PageActions> </PageActions>
)} )}
</PageHeader> </PageHeader>
<PageContent className="mt-10"> <PageContent>
{agents.length > 0 ? {agents.length > 0 ? (
<CardsWithPagination <CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
data={agents} ) : (
cardItem={AgentCard} <EmptyStatePlaceholder
cardsPerPage={4} url={"/dashboard/agents/new"}
numberOfColumns={1} text={"Create new Agent"}
/> />
: )}
<Link
href={"/dashboard/agents/new"}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
Create new Agent
</Link>
}
</PageContent> </PageContent>
</Page> </Page>
) );
} }
+8 -11
View File
@@ -1,16 +1,13 @@
import React from "react"; import React from "react";
import {currentUser} from "@/auth/current-user"; import { notFound } from "next/navigation";
import {notFound} from "next/navigation"; import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
const user = await currentUser() if (user!.role !== "superadmin" && user!.role !== "admin") {
if(user.role != "admin"){ notFound();
notFound()
} }
return (
<> return <>{children}</>;
{children}
</>
)
} }
@@ -0,0 +1,41 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
import {currentUser} from "@/lib/auth/current-user";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
export default async function RoutePage(
props: PageParams<{
databaseId: string;
}>
) {
const {databaseId} = await props.params;
const user = await currentUser();
if (!user) notFound();
const [dbToRestore] = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.id, databaseId));
if (!dbToRestore) {
notFound();
}
const dbsOfSameType = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.dbms!, dbToRestore.dbms!));
const successfulBackups = await db.select().from(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.status, "success"));
return (
<Page>
<PageHeader>
<PageTitle>Restore {dbToRestore.name}</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,36 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const dbItem = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
});
if (!dbItem) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {dbItem.name}</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm
databaseId={databaseId}
// @ts-ignore
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,119 @@
import {PageParams} from "@/types/next";
import {notFound, redirect} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
import {db} from "@/db";
import {eq, and, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services";
import {getOrganization} from "@/lib/auth/auth";
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {capitalizeFirstLetter} from "@/utils/text";
export default async function RoutePage(props: PageParams<{
projectId: string;
databaseId: string
}>) {
const {projectId, databaseId} = await props.params;
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const databasesProject = await getOrganizationProjectDatabases({
organizationSlug: organization.slug,
projectId: projectId
})
const dbItem = await db.query.database.findFirst({
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
with: {
project: true,
retentionPolicy: true
}
});
if (!dbItem) {
redirect("/dashboard/projects");
}
const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
with: {
restorations: true,
},
orderBy: (b, {desc}) => [desc(b.createdAt)],
});
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
orderBy: (r, {desc}) => [desc(r.createdAt)],
});
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
.then(rows => rows.length);
const availableBackups = backups.filter(b => !b.deletedAt).length;
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(and(
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
eq(drizzleDb.schemas.backup.status, "success")
))
.then(rows => rows.length);
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
notFound();
}
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full">
<div className=" w-full md:w-fit">
{capitalizeFirstLetter(dbItem.name)}
</div>
<div className="flex items-center gap-2 md:justify-between w-full">
<div className="flex items-center gap-2">
{/* Do not delete*/}
{/*<EditButton/>*/}
<RetentionPolicySheet database={dbItem}/>
<CronButton database={dbItem}/>
</div>
<div className="flex items-center gap-2">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</div>
</div>
</PageTitle>
</div>
{dbItem.description && (
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
)}
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
totalBackups={totalBackups}/>
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,65 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const {projectId} = await props.params;
const proj = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
with: {
databases: true,
},
});
if (!proj) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
)
// .filter((db) => db.project !== null) as DatabaseWith[];
return (
<Page>
<PageHeader>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm
organization={org}
databases={availableDatabases as DatabaseWith[]}
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,88 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {notFound, redirect} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text";
export default async function RoutePage(props: PageParams<{
projectId: string
}>) {
const {
projectId
} = await props.params;
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
const proj = await db.query.project.findFirst({
where: (proj, {
and,
eq,
not
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
with: {
databases: true,
},
});
if (!proj) {
redirect("/dashboard/projects");
}
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{capitalizeFirstLetter(proj.name)}
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</PageActions>
</div>
<PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? (
<CardsWithPagination
data={proj.databases}
organizationSlug={organization.slug}
// @ts-ignore
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={proj}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
<p className="text-lg font-medium">No databases found</p>
<p className="text-sm mt-2">You havent added any databases to this project yet.</p>
</div>
)}
</PageContent>
</Page>
);
}
@@ -0,0 +1,49 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ }>) {
const organization = await getOrganization({});
if (!organization ) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={org}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,58 @@
import Link from "next/link";
import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
import {db} from "@/db";
import {notFound} from "next/navigation";
import {getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
export default async function RoutePage(props: PageParams<{ }>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const projects = await db.query.project.findMany({
where: (project, {
eq,
and,
not
}) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))),
with: {
databases: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>Projects</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent>
{projects.length > 0 ? (
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
cardsPerPage={4} numberOfColumns={1}/>
) : (
<EmptyStatePlaceholder
url={"/dashboard/projects/new"}
text={"Create new Project"}
/>
)}
</PageContent>
</Page>
);
}
@@ -0,0 +1,38 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
import {getOrganization} from "@/lib/auth/auth";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import {currentUser} from "@/lib/auth/current-user";
export default async function RoutePage(props: PageParams<{
}>) {
const user = await currentUser();
const organization = await getOrganization({});
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
if (!user || !users || !organization || organization.slug == "default") {
notFound();
}
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,53 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {currentUser} from "@/lib/auth/current-user";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
import {
SettingsOrganizationMembersTable
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({});
const user = await currentUser();
const activeMember = await getActiveMember()
if (!organization) {
notFound();
}
const isMember = activeMember?.role === "member";
if (isMember) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Organization settings
{!isMember && organization.slug !== "default" && (
<EditButtonSettings/>
)}
</PageTitle>
<PageActions>
{!isMember && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/>
)}
</PageActions>
</PageHeader>
{/*<PageDescription>*/}
{/* Manage your organization settings.*/}
{/*</PageDescription>*/}
<PageContent>
<SettingsOrganizationMembersTable organization={organization}/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,202 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {and, asc, count, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth";
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
const projects = await db.query.project.findMany({
where: eq(drizzleDb.schemas.project.organizationId, org.id),
});
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
// const tomorrow = new Date();
// tomorrow.setDate(tomorrow.getDate() + 1);
//
// const before = new Date();
// before.setDate(before.getDate() - 1);
//
// const backupsEvolution = [
// {
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date(before)
// },
// {
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
// createdAt: new Date(before)
// },
// {
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
// createdAt: new Date()
// },
// {
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
// createdAt: new Date()
// },
// {
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
// createdAt: new Date()
// },
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
// createdAt: new Date()
// },
// {
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
// createdAt: new Date()
// },
//
// // Entries with tomorrow's date
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
// createdAt: new Date(tomorrow)
// }
// ];
const backupsRate = await db
.select({
createdAt: drizzleDb.schemas.backup.createdAt,
status: drizzleDb.schemas.backup.status,
_count: count(),
})
.from(drizzleDb.schemas.backup)
.where(and(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]), inArray(drizzleDb.schemas.backup.databaseId, databaseIds)))
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
.orderBy(drizzleDb.schemas.backup.createdAt);
const restorationsCountResult = await db
.select({
count: count(),
})
.from(drizzleDb.schemas.restoration)
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
const projectsCount = projects.length;
const backupsEvolutionCount = backupsEvolution.length;
const sortedBackupsEvolution = backupsEvolution.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
return (
<Page>
<PageHeader>
<PageTitle>Statistics</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<Folder className="w-5 h-5 text-muted-foreground" />
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
</Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
<CardTitle>Backups</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
</Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
<CardTitle>Restorations</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={sortedBackupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
</div>
</PageContent>
</Page>
);
}
@@ -1,55 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
const databases = await prisma.database.findMany({
where: {
dbms: database.dbms,
}
})
const backups = await prisma.backup.findMany({
where: {
status: "success",
}
});
if (!database) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Restore {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
</PageContent>
</Page>
)
}
@@ -1,17 +0,0 @@
import {PageParams} from "@/types/next";
import {redirect} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
// return (
// <div className="flex flex-1 flex-col gap-4 p-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// )
}
@@ -1,39 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {DatabaseForm} from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
if (!database) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Edit {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
</PageContent>
</Page>
)
}
@@ -1,91 +0,0 @@
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const {databaseId} = await props.params
const database = await prisma.database.findUnique({
where: {
id: databaseId,
},
})
if (!database) {
notFound()
}
const backups = await prisma.backup.findMany({
where: {
databaseId: database.id
},
include:{
restorations: {}
},
orderBy: {
createdAt: "desc"
}
})
const restorations = await prisma.restoration.findMany({
where: {
databaseId: databaseId,
},
orderBy: {
createdAt: "desc"
}
})
const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting");
const isAlreadyRestore = !!restorations.find(restoration => restoration.status === "waiting");
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
},
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{database.name}
<EditButton/>
<CronButton database={database}/>
</PageTitle>
<PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi
successRate={successRate}
database={database}
totalBackups={totalBackups}
/>
<DatabaseTabs
database={database}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}
/>
</PageContent>
</Page>
)
}
@@ -1,61 +0,0 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
export default async function RoutePage(props: PageParams<{
projectId: string;
}>) {
const {projectId} = await props.params
const project = await prisma.project.findUnique({
where: {
id: projectId,
},
include: {
databases: {}
}
});
if (!project) {
notFound();
}
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
const availableDatabases = await prisma.database.findMany({
where: {
OR: [
{projectId: null},
{projectId: project.id},
],
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
return (
<Page>
<PageHeader>
<PageTitle>
Edit {project.name}
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project}
projectId={project.id}/>
</PageContent>
</Page>
)
}
@@ -1,77 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {prisma} from "@/prisma";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
const {slug: organizationSlug, projectId} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const project = await prisma.project.findUnique({
where: {
id: projectId,
isArchived: {not: true}
},
include:{
databases: {}
}
})
if(!project){
notFound()
}
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{project.name}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject
projectId={projectId}
text={"Delete Project"}
/>
</PageActions>
</div>
<PageDescription>
The list of associated databases
</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{project.databases.length > 0 ?
<CardsWithPagination
data={project.databases}
organizationSlug={organizationSlug}
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={project}
/>
:
null
}
</PageContent>
</Page>
)
}
@@ -1,47 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const availableDatabases = await prisma.database.findMany({
where: {
projectId: null
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
const organization = await prisma.organization.findFirst({
where: {
slug: currentOrganizationSlug,
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Create new project
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={organization}/>
</PageContent>
</Page>
)
}
@@ -1,81 +0,0 @@
import Link from 'next/link'
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import {requiredCurrentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {currentOrganization} from "@/auth/current-organization";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const projects = await prisma.project.findMany({
where: {
isArchived: {not: true},
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
},
include: {
databases: {}
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Projects
</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/${currentOrganizationSlug}/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ?
<CardsWithPagination
organizationSlug={currentOrganizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link
href={`/dashboard/${currentOrganizationSlug}/projects/new`}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
Create new Project
</Link>
}
</PageContent>
</Page>
)
}
@@ -1,62 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {prisma} from "@/prisma";
import {requiredCurrentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
export default async function RoutePage(props: PageParams<{
slug: string;
}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const user = await requiredCurrentUser()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const organization = await prisma.organization.findUnique({
where:{
slug: currentOrganizationSlug,
},
include:{
users:{
include:{
user:{
}
},
where: {
user: {
id: {
not: user.id, // Exclude user with id: 1
},
},
},
}
}
})
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
id: {not: user.id}
}
})
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -1,74 +0,0 @@
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const organization = await prisma.organization.findUnique({
where: {
slug: currentOrganizationSlug,
},
include: {
users:{
include:{
user: {}
}
}
}
})
const currentOrganizationUser = await prisma.userOrganization.findFirst({
where:{
userId: user.id,
organization:{
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
}
}
})
if (currentOrganizationUser.role != "admin") {
notFound()
}
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Settings
{organization.slug != "default" ?
<EditButtonSettings/>
: null}
</PageTitle>
<PageActions>
{organization.slug != "default" ?
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
: null}
</PageActions>
</PageHeader>
<PageDescription>
Manage your organization settings.
</PageDescription>
<PageContent>
<SettingsTabs settings={settings} currentUser={user} users={organization.users}/>
</PageContent>
</Page>
)
}
@@ -1,93 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
export default async function RoutePage(props: PageParams<{}>) {
const projectsCount = await prisma.project.count({
where: {
organization: {
slug: "default"
}
},
});
const backupsEvolution = await prisma.backup.findMany({
select: {
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
});
const backupsRate = await prisma.backup.groupBy({
by: ['createdAt', 'status'],
where: {
status: {
in: ['success', 'failed'],
},
},
_count: {
id: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>
Statistics
</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>
{projectsCount}
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
</div>
</PageContent>
</Page>
)
}
+134
View File
@@ -0,0 +1,134 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
import {currentUser} from "@/lib/auth/current-user";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {asc, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {listOrganizations} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
const organizations = await listOrganizations()
if (!user || !organizations) notFound();
const organizationIds = organizations.map(project => project.id);
const agents = await db.query.agent.findMany({});
const projects = await db.query.project.findMany({
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
});
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
deletedAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
const availableBackups = backupsEvolution.filter(backup => backup.deletedAt == null);
return (
<Page>
<PageHeader>
<PageTitle>Dashboard</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Organizations</CardTitle>
<Building2 className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{organizations.length}</div>
<p className="text-xs text-muted-foreground">Number of organizations</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Projects</CardTitle>
<Folder className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projects.length}</div>
<p className="text-xs text-muted-foreground">Number of projects</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Databases</CardTitle>
<Database className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{databasesOfAllProjects.length}</div>
<p className="text-xs text-muted-foreground">Databases across all projects</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Agents</CardTitle>
<Workflow className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{agents.length}</div>
<p className="text-xs text-muted-foreground">Registered agents</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Backups</CardTitle>
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{backupsEvolution.length}</div>
<p className="text-xs text-muted-foreground">All backups recorded</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Available Backups</CardTitle>
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{availableBackups.length}</div>
<p className="text-xs text-muted-foreground">Currently active backups</p>
</CardContent>
</Card>
</div>
{/*Do not delete*/}
{/*<div className="flex flex-1 flex-col gap-4">*/}
{/* <div className="grid auto-rows-min gap-4 md:grid-cols-3">*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* </div>*/}
{/*</div>*/}
</PageContent>
</Page>
);
}
+7 -26
View File
@@ -1,45 +1,26 @@
import React from "react"; import React from "react";
import {redirect} from "next/navigation"; import {redirect} from "next/navigation";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar" import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar"; import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
import {currentUser} from "@/auth/current-user";
import {Header} from "@/features/layout/Header"; import {Header} from "@/features/layout/Header";
import {prisma} from "@/prisma"; import {currentUser} from "@/lib/auth/current-user";
import {GlobalStoreProvider} from "@/state-management/provider";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser();
const user = await currentUser() if (!user) redirect("/login");
if (user) {
const userInfo = await prisma.user.findUnique({
where: {
email: user?.email
}
})
if (!userInfo) redirect('/login')
}
if (!user) redirect('/login')
return ( return (
// <HydrationZustand>
// <GlobalStoreProvider>
<> <>
<SidebarProvider> <SidebarProvider>
<div className="flex flex-col lg:flex-row w-full"> <div className="flex flex-col lg:flex-row w-full">
<AppSidebar/> <AppSidebar/>
<SidebarInset> <SidebarInset>
<Header/> <Header/>
<main className="h-full"> <main className="h-full">{children}</main>
{children}
</main>
</SidebarInset> </SidebarInset>
</div> </div>
</SidebarProvider> </SidebarProvider>
</> </>
// </GlobalStoreProvider> );
// </HydrationZustand>
)
} }
+2 -2
View File
@@ -1,9 +1,9 @@
import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner"; import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner";
export default function Loading() { export default function Loading() {
return ( return (
<div className="w-full h-full flex flex-col items-center justify-center"> <div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50} /> <LoadingSpinner size={50} />
</div> </div>
) );
} }
+50 -32
View File
@@ -1,44 +1,62 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {requiredCurrentUser} from "@/auth/current-user"; import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import {prisma} from "@/prisma";
import {Badge} from "@/components/ui/badge"; import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount"; import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload"; import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
import {currentUser} from "@/lib/auth/current-user";
import {getAccounts, getSessions} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
const user = await requiredCurrentUser()
if (!user) { if (!user) {
notFound() return notFound();
} }
const userInfo = await prisma.user.findUnique({
where: { if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
email: user.email return notFound();
} }
})
const sessions = await getSessions();
const accounts = await getAccounts();
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
<AvatarWithUpload user={userInfo}/> <AvatarWithUpload
{user.name} user={{
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge> ...user,
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge> image: user.image ?? null,
</PageTitle> role: user.role ?? null,
<PageActions className="mt-2 hidden sm:block"> banned: user.banned ?? null,
<ButtonDeleteAccount text="Delete my account"/> banReason: user.banReason ?? null,
</PageActions> banExpires: user.banExpires ?? null,
</div> deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
<PageContent> }}
<UserForm userId={userInfo.id} defaultValues={userInfo}/> />
<div className="mt-4 sm:hidden"> {user.name}
<ButtonDeleteAccount text="Delete my account" /> <Badge className="ml-3 hidden lg:block">{user.role}</Badge>
</div> </PageTitle>
{/*<PageActions className="mt-2 hidden sm:block">*/}
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
{/*</PageActions>*/}
</div>
<PageContent >
<UserForm
userId={user.id}
sessions={sessions} accounts={accounts}
defaultValues={{
name: user.name,
email: user.email,
role: user.role ?? undefined,
}}
/>
{/*<div className="mt-4 sm:hidden ">*/}
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
{/*</div>*/}
</PageContent> </PageContent>
</Page> </Page>
) );
} }
+5 -5
View File
@@ -1,7 +1,7 @@
export default function Home() { export default function Home() {
return ( return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-3xl font-bold text-4xl leading-tight">Home landing page</h1> <h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
</div> </div>
); );
} }
+7 -9
View File
@@ -1,16 +1,14 @@
import {redirect} from "next/navigation"; import { redirect } from "next/navigation";
import {currentUser} from "@/auth/current-user"; import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; import { currentUser } from "@/lib/auth/current-user";
export default async function Index() { export default async function Index() {
const user = await currentUser() const user = await currentUser();
if (user) { if (user) {
const currentOrganizationSlug = await getCurrentOrganizationSlug() const currentOrganizationSlug = await getCurrentOrganizationSlug();
redirect(`/dashboard/home`);
redirect(`/dashboard/${currentOrganizationSlug}/projects`)
} }
redirect("/login") redirect("/login");
//Do not delete //Do not delete
// return ( // return (
// <Home/> // <Home/>
+78 -53
View File
@@ -1,25 +1,29 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {prisma} from "@/prisma";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {uploadLocalPrivate} from "@/features/upload/private/upload.action"; import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {Backup, Database} from "@prisma/client";
import {eventEmitter} from "../../../events/route"; import {eventEmitter} from "../../../events/route";
import {db} from "@/db";
import {Backup} from "@/db/schema/07_database";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {env} from "@/env.mjs";
import {withUpdatedAt} from "@/db/utils";
export async function POST( export async function POST(
request: Request, request: Request,
{ params }: { params: Promise<{ agentId: string }> } {params}: { params: Promise<{ agentId: string }> }
) { ) {
try { try {
const contentType = request.headers.get("Content-Type"); const contentType = request.headers.get("Content-Type");
if (!contentType || !contentType.includes("multipart/form-data")) { if (!contentType || !contentType.includes("multipart/form-data")) {
return NextResponse.json( return NextResponse.json(
{ error: "Unsupported or missing Content-Type" }, {error: "Unsupported or missing Content-Type"},
{ status: 400 } {status: 400}
); );
} }
eventEmitter.emit('modification', { update: true }); eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId; const agentId = (await params).agentId;
const formData = await request.formData(); const formData = await request.formData();
@@ -28,61 +32,67 @@ export async function POST(
if (!generatedId || !isUuidv4(generatedId)) { if (!generatedId || !isUuidv4(generatedId)) {
return NextResponse.json( return NextResponse.json(
{ error: "generatedId is not a valid UUID" }, {error: "generatedId is not a valid UUID"},
{ status: 400 } {status: 400}
); );
} }
const agent = await prisma.agent.findFirst({ const agent = await db.query.agent.findFirst({
where: { id: agentId }, where: eq(drizzleDb.schemas.agent.id, agentId),
}); });
if (!agent) { if (!agent) {
return NextResponse.json( return NextResponse.json(
{ error: "Agent not found" }, {error: "Agent not found"},
{ status: 404 } {status: 404}
); );
} }
const database = await prisma.database.findFirst({ const database = await db.query.database.findFirst({
where: { generatedId }, where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: {
project: true
}
}); });
if (!database) { if (!database) {
return NextResponse.json( return NextResponse.json(
{ error: "Database associated with generatedId not found" }, {error: "Database associated with generatedId not found"},
{ status: 404 } {status: 404}
); );
} }
let backup: Backup | null = null; let backup: Backup | null | undefined = null;
if (method === "automatic") { if (method === "automatic") {
backup = await prisma.backup.create({ [backup] = await db
data: { .insert(drizzleDb.schemas.backup)
status: "ongoing", .values({
status: 'ongoing',
databaseId: database.id, databaseId: database.id,
}, })
}); .returning();
if (!backup) { if (!backup) {
return NextResponse.json( return NextResponse.json(
{ error: "Unable to create an automatic backup" }, {error: "Unable to create an automatic backup"},
{ status: 500 } {status: 500}
); );
} }
} else { } else {
backup = await prisma.backup.findFirst({ backup = await db.query.backup.findFirst({
where: { where: and(
status: "ongoing", eq(drizzleDb.schemas.backup.status, 'ongoing'),
databaseId: database.id, eq(drizzleDb.schemas.backup.databaseId, database.id),
}, ),
}); });
if (!backup) { if (!backup) {
return NextResponse.json( return NextResponse.json(
{ error: "Unable to find the corresponding backup" }, {error: "Unable to find the corresponding backup"},
{ status: 404 } {status: 404}
); );
} }
} }
@@ -94,8 +104,8 @@ export async function POST(
if (!file) { if (!file) {
return NextResponse.json( return NextResponse.json(
{ error: "File is required for successful backup" }, {error: "File is required for successful backup"},
{ status: 400 } {status: 400}
); );
} }
@@ -103,49 +113,64 @@ export async function POST(
const fileName = `${uuid}.dump`; const fileName = `${uuid}.dump`;
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
const { success, message, filePath } = await uploadLocalPrivate(fileName, buffer); const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
throw new Error("System settings not found.");
}
let success: boolean, message: string, filePath: string;
const result =
settings.storage === "local"
? await uploadLocalPrivate(fileName, buffer)
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
({success, message, filePath} = result);
if (!success) { if (!success) {
return NextResponse.json( return NextResponse.json(
{ error: message }, {error: message},
{ status: 500 } {status: 500}
); );
} }
await prisma.backup.update({ await db
where: { id: backup.id }, .update(drizzleDb.schemas.backup)
data: { .set(withUpdatedAt({
file: fileName, file: fileName,
status: "success", status: 'success',
}, }))
}); .where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
return NextResponse.json( return NextResponse.json(
{ {
message: "Backup successfully uploaded", message: "Backup successfully uploaded",
}, },
{ status: 200 } {status: 200}
); );
} else { } else {
await prisma.backup.update({
where: { id: backup.id }, await db
data: { status: "failed" }, .update(drizzleDb.schemas.backup)
}); .set(withUpdatedAt({status: 'failed'}))
eventEmitter.emit('modification', { update: true }); .where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
return NextResponse.json( return NextResponse.json(
{ {
message: "Backup successfully updated with status failed", message: "Backup successfully updated with status failed",
}, },
{ status: 200 } {status: 200}
); );
} }
} catch (error) { } catch (error) {
console.error("Error in POST handler:", error); console.error("Error in POST handler:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Internal server error" }, {error: "Internal server error"},
{ status: 500 } {status: 500}
); );
} }
} }
+20 -27
View File
@@ -1,14 +1,17 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {prisma} from "@/prisma";
import {eventEmitter} from "../../../events/route"; import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
export type BodyResultRestore = { export type BodyResultRestore = {
generatedId: string generatedId: string
status: string status: string
} }
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
export async function POST( export async function POST(
@@ -17,13 +20,12 @@ export async function POST(
) { ) {
try { try {
eventEmitter.emit('modification', { update: true }); eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId const agentId = (await params).agentId
const body: BodyResultRestore = await request.json(); const body: BodyResultRestore = await request.json();
console.log(body) console.log(body)
console.log(agentId)
if (!isUuidv4(body.generatedId)) { if (!isUuidv4(body.generatedId)) {
@@ -33,53 +35,44 @@ export async function POST(
); );
} }
const agent = await prisma.agent.findFirst({ const agent = await db.query.agent.findFirst({
where: { where: eq(drizzleDb.schemas.agent.id, agentId)
id: agentId
}
}) })
if (!agent) { if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404}) return NextResponse.json({error: "Agent not found"}, {status: 404})
} }
const database = await prisma.database.findFirst({ const database = await db.query.database.findFirst({
where: { where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId)
generatedId: body.generatedId
}
}) })
if (!database) { if (!database) {
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404}) return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
} }
const restoration = await prisma.restoration.findFirst({ const restoration = await db.query.restoration.findFirst({
where: { where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
status : "ongoing",
databaseId: database.id
}
}) })
if (!restoration) { if (!restoration) {
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404}) return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
} }
await prisma.restoration.update({ await db
where:{ .update(drizzleDb.schemas.restoration)
id : restoration.id .set({ status: body.status as RestorationStatus })
}, .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
data: {
status: body.status
}
})
const response = { const response = {
message: true, message: true,
details: "Restoration successfully updated" details: "Restoration successfully updated"
} }
eventEmitter.emit('modification', { update: true }); eventEmitter.emit('modification', {update: true});
return Response.json(response , {status: 200}) return Response.json(response, {status: 200})
} catch (error) { } catch (error) {
+110 -64
View File
@@ -1,16 +1,23 @@
import {Agent, Database} from "@prisma/client";
import {prisma} from "@/prisma";
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {Body} from "./route"; import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action"; import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
import {Agent} from "@/db/schema/08_agent";
import {Database} from "@/db/schema/07_database";
import * as drizzleDb from "@/db";
import {db as dbClient} from "@/db";
import {and, eq} from "drizzle-orm";
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
import {ServerActionResult} from "@/types/action-type";
import {SafeActionResult} from "next-safe-action";
import {ZodString} from "zod";
import {withUpdatedAt} from "@/db/utils";
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) { export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
const databasesResponse = []; const databasesResponse = [];
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({ const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
generatedId: database.generatedId, generatedId: database.agentDatabaseId,
dbms: database.dbms, dbms: database.dbms,
data: { data: {
backup: { backup: {
@@ -25,97 +32,136 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
}); });
for (const db of body.databases) { for (const db of body.databases) {
const existingDatabase = await prisma.database.findFirst({
where: { const existingDatabase = await dbClient.query.database.findFirst({
generatedId: db.generatedId, where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
},
}); });
let backupAction: boolean = false let backupAction: boolean = false
let restoreAction: boolean = false let restoreAction: boolean = false
let UrlBackup: string = null let urlBackup: string = ""
if (!existingDatabase) { if (!existingDatabase) {
if (!isUuidv4(db.generatedId)) { if (!isUuidv4(db.generatedId)) {
return NextResponse.json( return NextResponse.json(
{ error: "generatedId is not a valid uuid" }, {error: "generatedId is not a valid uuid"},
{ status: 500 } {status: 500}
); );
} }
const databaseCreated = await prisma.database.create({ const [databaseCreated] = await dbClient
data: { .insert(drizzleDb.schemas.database)
.values({
agentId: agent.id, agentId: agent.id,
name: db.name, name: db.name,
dbms: db.dbms, dbms: db.dbms as EDbmsSchema,
generatedId: db.generatedId, agentDatabaseId: db.generatedId,
lastContact: lastContact, lastContact: lastContact,
}, })
}); .returning();
if (databaseCreated) { if (databaseCreated) {
databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup)); databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
} }
} else { } else {
const databaseUpdated = await prisma.database.update({
where: {
id: existingDatabase.id,
},
data: {
lastContact: lastContact,
},
});
const backup = await prisma.backup.findFirst({
where: {
databaseId: databaseUpdated.id,
status: "waiting"
}
})
const restoration = await prisma.restoration.findFirst({ const [databaseUpdated] = await dbClient
where:{ .update(drizzleDb.schemas.database)
databaseId: databaseUpdated.id, .set({lastContact: lastContact})
status: "waiting" .where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
} .returning();
const backup = await dbClient.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
}) })
if(backup){ const restoration = await dbClient.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
})
if (backup) {
backupAction = true backupAction = true
await prisma.backup.update({
where:{ await dbClient
id: backup.id .update(drizzleDb.schemas.backup)
}, .set(withUpdatedAt({status: "ongoing"}))
data: { .where(eq(drizzleDb.schemas.backup.id, backup.id));
status: "ongoing"
}
})
} }
if(restoration){ if (restoration) {
restoreAction = true restoreAction = true
const backupToRestore = await prisma.backup.findFirst({
where:{ const backupToRestore = await dbClient.query.backup.findFirst({
id: restoration.backupId where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
with: {
database: {
with: {
project: true
}
}
} }
}) })
const fileName = backupToRestore.file
UrlBackup = await getFileUrlPresignedLocal(fileName) const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
await prisma.restoration.update({ if (!settings) {
where: { return NextResponse.json(
id: restoration.id {error: "Unable to find settings"},
}, {status: 500}
data:{ );
status: "ongoing" }
const fileName = backupToRestore?.file
let data: SafeActionResult<string, ZodString, readonly [], {
_errors?: string[] | undefined;
}, readonly [], ServerActionResult<string>, object> | undefined
try {
if (settings.storage == "local") {
data = await getFileUrlPresignedLocal(fileName!)
} else if (settings.storage == "s3") {
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
} }
})
if (data?.data?.success) {
urlBackup = data.data.value ?? "";
} else {
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
// @ts-ignore
const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
console.error("Restoration failed: ", errorMessage);
continue;
}
} catch (err) {
console.error("Restoration crashed unexpectedly:", err);
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
continue;
}
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "ongoing"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
} }
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup));
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, UrlBackup));
} }
} }
return databasesResponse; return databasesResponse;
} }
+32 -19
View File
@@ -1,14 +1,16 @@
import {prisma} from "@/prisma";
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {Dbms} from "@prisma/client";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action"; import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
import {handleDatabases} from "./helpers"; import {handleDatabases} from "./helpers";
import {eventEmitter} from "../../../events/route"; import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {EDbmsSchema} from "@/db/schema/types";
import {eq} from "drizzle-orm";
import {isUuidv4} from "@/utils/verify-uuid";
export type databaseAgent = { export type databaseAgent = {
name: string, name: string,
dbms: Dbms, dbms: EDbmsSchema,
generatedId: string generatedId: string
} }
@@ -16,7 +18,7 @@ export type Body = {
databases: databaseAgent[] databases: databaseAgent[]
} }
// Function to test the get file url presigned local
export async function GET(request: Request) { export async function GET(request: Request) {
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump") const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
return Response.json({ return Response.json({
@@ -32,26 +34,36 @@ export async function POST(
const agentId = (await params).agentId const agentId = (await params).agentId
const body: Body = await request.json(); const body: Body = await request.json();
const lastContact = new Date(); const lastContact = new Date();
let message: string
const agent = await prisma.agent.findFirst({
where: { if (!isUuidv4(agentId)) {
id: agentId message = "agentId is not a valid uuid"
} console.log(message)
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
);
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
}) })
if (!agent) { if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404}) message = "Agent not found"
console.log(message)
return NextResponse.json({error: message}, {status: 404})
} }
const databasesResponse = await handleDatabases(body, agent, lastContact) const databasesResponse = await handleDatabases(body, agent, lastContact)
await prisma.agent.update({
where:{
id: agentId,
},
data:{
lastContact: lastContact,
}
})
eventEmitter.emit('modification', { update: true });
await db
.update(drizzleDb.schemas.agent)
.set({lastContact: lastContact})
.where(eq(drizzleDb.schemas.agent.id, agentId));
eventEmitter.emit('modification', {update: true});
const response = { const response = {
agent: { agent: {
@@ -61,6 +73,7 @@ export async function POST(
databases: databasesResponse databases: databasesResponse
} }
console.log(response) console.log(response)
return Response.json(response) return Response.json(response)
} catch (error) { } catch (error) {
console.error('Error in POST handler:', error); console.error('Error in POST handler:', error);
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
-4
View File
@@ -1,4 +0,0 @@
import {handlers} from "@/auth/auth";
export const {GET, POST} = handlers;
+1 -1
View File
@@ -31,7 +31,7 @@ export async function GET(
{status: 403} {status: 403}
); );
} }
//@ts-ignore
const expiresAt = parseInt(expires, 10); const expiresAt = parseInt(expires, 10);
if (Date.now() > expiresAt) { if (Date.now() > expiresAt) {
return NextResponse.json( return NextResponse.json(
+8 -4
View File
@@ -1,6 +1,6 @@
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs/promises";
import {NextResponse} from "next/server"; import { NextResponse } from "next/server";
export async function GET( export async function GET(
request: Request, request: Request,
@@ -8,14 +8,18 @@ export async function GET(
) { ) {
try { try {
const fileName = (await params).fileName const fileName = (await params).fileName;
console.log("fileName", fileName);
const filePath = path.join(process.cwd(), "private/uploads/images", fileName); const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists // Check if the file exists
try { try {
await fs.access(filePath); // Ensures the file exists await fs.access(filePath); // Ensures the file exists
} catch { } catch {
return NextResponse.json({error: "File not found"}, {status: 404}); return NextResponse.json({ error: "File not found" }, { status: 404 });
} }
// Read the file // Read the file
@@ -29,6 +33,6 @@ export async function GET(
}); });
} catch (error) { } catch (error) {
console.error("Error reading file:", error); console.error("Error reading file:", error);
return NextResponse.json({error: "Internal Server Error"}, {status: 500}); return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
} }
} }
+6 -15
View File
@@ -1,20 +1,14 @@
import {NextResponse} from "next/server"; import { NextResponse } from "next/server";
export type BodyInit = { export type BodyInit = {
initialize: boolean initialize: boolean;
} };
export async function POST(request: Request) {
export async function POST(
request: Request,
) {
try { try {
const body: BodyInit = await request.json(); const body: BodyInit = await request.json();
console.log(body) console.log(body);
return NextResponse.json( return NextResponse.json(
{ {
@@ -24,9 +18,6 @@ export async function POST(
); );
} catch (error) { } catch (error) {
console.error("Error in POST initialization:", error); console.error("Error in POST initialization:", error);
return NextResponse.json( return NextResponse.json({ error: "Internal server error" }, { status: 500 });
{ error: "Internal server error" },
{ status: 500 }
);
} }
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+2 -2
View File
@@ -24,7 +24,7 @@
// <div className="flex h-screen w-full flex-col items-center justify-center"> // <div className="flex h-screen w-full flex-col items-center justify-center">
// <a // <a
// href="#" // href="#"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700" // className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// > // >
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white"> // <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
// Something went wrong // Something went wrong
@@ -63,7 +63,7 @@ function AuthErrorPageContent() {
<div className="flex h-screen w-full flex-col items-center justify-center"> <div className="flex h-screen w-full flex-col items-center justify-center">
<a <a
href="#" href="#"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700" className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
> >
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white"> <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Something went wrong Something went wrong
Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+127 -90
View File
@@ -1,105 +1,142 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "tw-animate-css";
@tailwind utilities;
/*body {*/ @custom-variant dark (&:is(.dark *));
/* font-family: Arial, Helvetica, sans-serif;*/
/*}*/
@layer utilities { @theme inline {
/*.text-balance {*/ --color-background: var(--background);
/* text-wrap: balance;*/ --color-foreground: var(--foreground);
/*}*/ --font-sans: var(--font-geist-sans);
.popover-content-width-full { --font-mono: var(--font-geist-mono);
width: var(--radix-popover-trigger-width); --color-sidebar-ring: var(--sidebar-ring);
max-height: var(--radix-popover-content-available-height); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
} }
} }
:root {
--radius: 0.625rem;
@layer base { --background: oklch(1 0 0);
:root { --foreground: oklch(0.145 0 0);
--background: 0 0% 100%; --card: oklch(1 0 0);
--foreground: 240 10% 3.9%; --card-foreground: oklch(0.145 0 0);
--card: 0 0% 100%; --popover: oklch(1 0 0);
--card-foreground: 240 10% 3.9%; --popover-foreground: oklch(0.145 0 0);
--popover: 0 0% 100%; --primary: oklch(0.205 0 0);
--popover-foreground: 240 10% 3.9%; --primary-foreground: oklch(0.985 0 0);
--primary: 240 5.9% 10%; --secondary: oklch(0.97 0 0);
--primary-foreground: 0 0% 98%; --secondary-foreground: oklch(0.205 0 0);
--secondary: 240 4.8% 95.9%; --muted: oklch(0.97 0 0);
--secondary-foreground: 240 5.9% 10%; --muted-foreground: oklch(0.556 0 0);
--muted: 240 4.8% 95.9%; --accent: oklch(0.97 0 0);
--muted-foreground: 240 3.8% 46.1%; --accent-foreground: oklch(0.205 0 0);
--accent: 240 4.8% 95.9%; --destructive: oklch(0.577 0.245 27.325);
--accent-foreground: 240 5.9% 10%; --border: oklch(0.922 0 0);
--destructive: 0 84.2% 60.2%; --input: oklch(0.922 0 0);
--destructive-foreground: 0 0% 98%; --ring: oklch(0.708 0 0);
--border: 240 5.9% 90%; --chart-1: oklch(0.646 0.222 41.116);
--input: 240 5.9% 90%; --chart-2: oklch(0.6 0.118 184.704);
--ring: 240 5.9% 10%; --chart-3: oklch(0.398 0.07 227.392);
--radius: 0.5rem; --chart-4: oklch(0.828 0.189 84.429);
--chart-1: 12 76% 61%; --chart-5: oklch(0.769 0.188 70.08);
--chart-2: 173 58% 39%; --sidebar: oklch(0.985 0 0);
--chart-3: 197 37% 24%; --sidebar-foreground: oklch(0.145 0 0);
--chart-4: 43 74% 66%; --sidebar-primary: oklch(0.205 0 0);
--chart-5: 27 87% 67%; --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-background: 0 0% 98%; --sidebar-accent: oklch(0.97 0 0);
--sidebar-foreground: 240 5.3% 26.1%; --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-primary: 240 5.9% 10%; --sidebar-border: oklch(0.922 0 0);
--sidebar-primary-foreground: 0 0% 98%; --sidebar-ring: oklch(0.708 0 0);
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
} }
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
Executable
+3
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 90 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+9 -10
View File
@@ -1,17 +1,16 @@
import React from "react"; import React from "react";
import type {Metadata} from "next"; import type {Metadata} from "next";
import {Inter} from "next/font/google"; import {Inter} from "next/font/google";
import "./globals.css"; import "./globals.css";
import {Providers} from "./providers"; import {Providers} from "./providers";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
const inter = Inter({subsets: ["latin"]}); const inter = Inter({subsets: ["latin"]});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Portabase", title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
description: "Manage all your database instances from one place !", description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
}; };
export default function RootLayout({ export default function RootLayout({
@@ -21,12 +20,12 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body <head>
className={cn(inter.className, "h-full")} <meta name="apple-mobile-web-app-title" content="Portabase"/>
> </head>
<Providers> <body className={cn(inter.className, "h-full")}>
{children} <ConsoleSilencer/>
</Providers> <Providers>{children}</Providers>
</body> </body>
</html> </html>
); );
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Portabase",
"short_name": "Portabase",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+11 -12
View File
@@ -1,14 +1,13 @@
"use client"
import {useRouter} from "next/navigation";
import {useEffect} from "react";
export default function NotFound() { export default async function NotFound() {
return(
const router = useRouter(); <div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
<div className="w-full space-y-6 text-center">
useEffect(() => { <div className="space-y-3">
router.push('/'); <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
}, [router]); <p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
</div>
return null; // You can return null or a loading spinner while the redirect happens </div>
</div>
)
} }
+12 -18
View File
@@ -1,29 +1,23 @@
"use client" "use client";
import {PropsWithChildren} from "react"; import { PropsWithChildren, Suspense } from "react";
import {ThemeProvider} from "@/features/theme/theme-provider"; import { ThemeProvider } from "@/features/theme/theme-provider";
import {Toaster} from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {SessionProvider} from "next-auth/react";
export type ProviderProps = PropsWithChildren<{}>; export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient() const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => { export const Providers = (props: ProviderProps) => {
return ( return (
<SessionProvider> <Suspense fallback={null}>
<ThemeProvider <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
attribute="class"
defaultTheme="system"
enableSystem
// disableTransitionOnChange
>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Toaster/> <Toaster />
{props.children} {props.children}
</QueryClientProvider> </QueryClientProvider>
</ThemeProvider> </ThemeProvider>
</SessionProvider> </Suspense>
) );
} };
-1
View File
@@ -7,7 +7,6 @@ services:
context: . context: .
dockerfile: docker/dockerfile/Dockerfile dockerfile: docker/dockerfile/Dockerfile
target: prod target: prod
ports: ports:
- '8887:80' - '8887:80'
env_file: env_file:
+64 -59
View File
@@ -1,68 +1,73 @@
name: portabase-dev name: portabase-dev
services: services:
# app:
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: dev
# ports:
# - "8887:8887"
# environment:
# - TIME_ZONE="Europe/Paris"
# - NODE_ENV=development
# depends_on:
# db:
# condition: service_healthy
# volumes:
# - .:/app
# - /app/node_modules
# container_name: portabase-app
#
app: db:
build: image: postgres:16-alpine
context: . ports:
dockerfile: docker/dockerfile/Dockerfile - "5433:5432"
target: dev volumes:
ports: - postgres-data:/var/lib/postgresql/data
- '8887:80' environment:
environment: - POSTGRES_DB=devdb
- TIME_ZONE="Europe/Paris" - POSTGRES_USER=devuser
- NODE_ENV=development - POSTGRES_PASSWORD=changeme
depends_on: healthcheck:
db: test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
condition: service_healthy interval: 10s
volumes: timeout: 5s
- .:/app retries: 5
- /app/node_modules
container_name: portabase-app
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
# db-pgadmin:
# image: dpage/pgadmin4
# environment:
# PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
# PGADMIN_DEFAULT_PASSWORD: "changeme"
# PGADMIN_CONFIG_SERVER_MODE: "False"
# POSTGRES_USER: "devuser"
# POSTGRES_PASSWORD: "changeme"
# volumes:
# - pgadmin-data:/var/lib/pgadmin
# ports:
# - "8080:80"
# restart: unless-stopped
# depends_on:
# - db
s3:
container_name: s3-portabase-dev
image: docker.io/bitnami/minio:latest
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
- MINIO_BROWSER=on
volumes: volumes:
postgres-data: postgres-data:
minio_data: minio_data:
# pgadmin-data:
+26 -15
View File
@@ -1,12 +1,22 @@
FROM node:23.5.0-alpine AS base FROM node:22-alpine AS base
#ENV YARN_VERSION=4.2.2 ENV YARN_VERSION=4.9.1
ENV YARN_VERSION=4.5.3
RUN apk add --no-cache libc6-compat RUN apk add --update --no-cache \
RUN apk add --no-cache openssl build-base \
libc6-compat \
openssl \
make \
g++ \
jpeg-dev \
cairo-dev \
giflib-dev \
pango-dev \
libtool \
autoconf \
automake \
libpng
# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION} RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
RUN yarn set version ${YARN_VERSION} RUN yarn set version ${YARN_VERSION}
@@ -19,6 +29,7 @@ COPY .yarn ./.yarn
COPY .yarnrc.yml ./ COPY .yarnrc.yml ./
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \ RUN \
if [ -f yarn.lock ]; then yarn; \ if [ -f yarn.lock ]; then yarn; \
elif [ -f package-lock.json ]; then npm ci; \ elif [ -f package-lock.json ]; then npm ci; \
@@ -49,10 +60,6 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npx zenstack generate
RUN \ RUN \
if [ -f yarn.lock ]; then yarn run build; \ if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \ elif [ -f package-lock.json ]; then npm run build; \
@@ -74,18 +81,21 @@ RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
RUN mkdir .next RUN mkdir .next
RUN chown nextjs:nodejs .next
RUN mkdir -p /app/private/uploads RUN mkdir -p /app/private/uploads
RUN chown -R nextjs:nodejs /app/private RUN chown -R nextjs:nodejs /app/private
RUN chown -R nextjs:nodejs /app/public RUN chown -R nextjs:nodejs /app/public
RUN chown nextjs:nodejs .next
COPY --from=builder /app/next.config.mjs ./ COPY --from=builder /app/next.config.ts ./
COPY --from=builder /app/schema.zmodel ./ COPY --from=builder /app/portabase.config.ts ./
COPY --from=builder /app/src/openapi.yaml ./src/ COPY --from=builder /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs prisma ./prisma/ COPY --chown=nextjs:nodejs src/db ./src/db
USER root USER root
@@ -101,3 +111,4 @@ ENV HOSTNAME="0.0.0.0"
USER nextjs USER nextjs
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"] ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
+14 -9
View File
@@ -1,13 +1,18 @@
#!/bin/bash #!/bin/bash
#
#npx drizzle-kit generate
#npx drizzle-kit migrate
#
#npm run dev
#
#exec "$@"
set -euo pipefail
npm install -g npm@11.0.0 echo "▶ Running Drizzle codegen..."
npm -v npx drizzle-kit generate
echo "▶ Applying migrations..."
npx drizzle-kit migrate
current_date=$(date +"%Y-%m-%d") echo "▶ Starting Next.js dev server..."
npx zenstack generate exec npm run dev
npx prisma migrate dev --name $current_date
npm run dev
exec "$@"
+8 -10
View File
@@ -1,16 +1,14 @@
#!/bin/bash #!/bin/bash
echo " ____ __ __ " #echo " ____ __ __ "
echo " / __ \____ _____/ /_____ _/ /_ ____ _________ " #echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ " #echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ " #echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ " #echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
echo " " #echo " "
echo " Community Edition v1.0.0 " #echo " Community Edition v1.1.1 "
echo " " #echo " "
npx prisma migrate deploy
node server.js node server.js
exec "$@" exec "$@"
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({
path: ".env",
});
export default defineConfig({
out: "./src/db/migrations",
schema: ["./src/db/schema", "./src/db/schema/types.ts"],
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
+4 -11
View File
@@ -1,13 +1,6 @@
import { import { Button as ReactEmailButton } from "@react-email/components";
Button as ReactEmailButton import { ComponentPropsWithoutRef } from "react";
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => { export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
return ( return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
<ReactEmailButton };
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
{...props}
/>
)
}
+5 -27
View File
@@ -1,18 +1,7 @@
import { import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from "@react-email/components";
import * as React from "react"; import * as React from "react";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import {getServerUrl} from "@/utils/get-server-url"; import { getServerUrl } from "@/utils/get-server-url";
const baseUrl = getServerUrl(); const baseUrl = getServerUrl();
@@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
<Html> <Html>
<Head /> <Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>} {preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4" <Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
style={{fontFamily: "Arial, sans-serif"}}>
<Container className="bg-white border border-gray-200 p-12"> <Container className="bg-white border border-gray-200 p-12">
<Img <Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
src={`${baseUrl}/logo-title-black.png`} <Section>{children}</Section>
width="200"
height="auto"
alt="Logo"
/>
<Section>
{children}
</Section>
</Container> </Container>
</Body> </Body>
</Html> </Html>
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
); );
}; };
export default EmailLayout; export default EmailLayout;
+2 -5
View File
@@ -5,11 +5,8 @@ import * as React from "react";
export default function HelloEmail() { export default function HelloEmail() {
return ( return (
<Html> <Html>
<Button <Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
href="https://example.com" Hello,
style={{ background: "#000", color: "#fff" }}
>
Hello Charles
</Button> </Button>
</Html> </Html>
); );
+6 -32
View File
@@ -1,42 +1,16 @@
import { import { Text } from "@react-email/components";
Text,
} from "@react-email/components";
import * as React from "react"; import * as React from "react";
import EmailLayout from "./EmailLayout"; import EmailLayout from "./EmailLayout";
import {Button} from "@react-email/button"; import { Button } from "@react-email/button";
export const TestEmailSettings = () => { export const TestEmailSettings = () => {
return ( return (
<EmailLayout <EmailLayout preview="Email Setup">
preview="Email Setup" <Text className="text-base font-light leading-8 text-green-800 ">Hi, your email settings are setup !</Text>
> <Text className="text-base font-light leading-8 text-green-800 ">Best regard,</Text>{" "}
<Text className="text-base font-light leading-8 text-green-800 ">Portabase</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Hi, your email settings are setup !
</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Best regard,
</Text> <Text
className="text-base font-light leading-8 text-green-800 "
>
Portabase
</Text>
</EmailLayout> </EmailLayout>
); );
}; };
export default TestEmailSettings; export default TestEmailSettings;
+4 -11
View File
@@ -1,13 +1,6 @@
import { import { Text as ReactEmailText } from "@react-email/components";
Text as ReactEmailText import { ComponentPropsWithoutRef } from "react";
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => { export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return ( return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
<ReactEmailText };
className="text-base font-light leading-8 text-green-800 "
{...props}
/>
)
}
+5 -5
View File
@@ -1,6 +1,6 @@
import {init} from "@/utils/init"; export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const init = await import("@/utils/init");
export function register() { await init.init();
init(); }
} }
+50 -21
View File
@@ -1,43 +1,70 @@
import {NextRequest, NextResponse} from 'next/server' import { NextRequest, NextResponse } from "next/server";
import {loggingMiddleware} from "@/middleware/loggingMiddleware"; import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler"; import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
import { signOut } from "@/lib/auth/auth-client";
export function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
if (url.pathname.startsWith("/dashboard")) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
}
if (session.user.banned) {
signOut();
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
signOut();
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
}
if (url.pathname === "/dashboard") {
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
}
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith('/api/auth')) {
return NextResponse.next(); return NextResponse.next();
} }
if (url.pathname.startsWith('/api')) { // Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname); const routeExists = checkRouteExists(url.pathname);
// If the route does not exist, return a 404 JSON response // If the route does not exist, return a 404 JSON response
if (!routeExists) { if (!routeExists) {
return new NextResponse( return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
JSON.stringify({ message: "This API route does not exist.", status: 404 }), status: 404,
{ status: 404, headers: { 'Content-Type': 'application/json' } } headers: { "Content-Type": "application/json" },
); });
} }
} }
try{ try {
loggingMiddleware(request); loggingMiddleware(request);
}catch(err){ } catch (err) {
errorHandler(err) errorHandler(err);
} }
} }
// Function to check if the route exists (supports dynamic routes) // Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname) { function checkRouteExists(pathname: string) {
// Define static and dynamic routes with patterns // Define static and dynamic routes with patterns
const routePatterns = [ const routePatterns = [
//do not delete //do not delete
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username) // /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status /^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/agent\/[^/]+\/backup\/?$/, /^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/, /^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/[^/]+\/?$/, /^\/api\/files\/[^/]+\/?$/,
@@ -45,13 +72,15 @@ function checkRouteExists(pathname) {
/^\/api\/events\/?$/, /^\/api\/events\/?$/,
/^\/api\/init\/?$/, /^\/api\/init\/?$/,
]; ];
return routePatterns.some(pattern => pattern.test(pathname)); return routePatterns.some((pattern) => pattern.test(pathname));
} }
export const config = { export const config = {
runtime: "nodejs",
matcher: [ matcher: [
// '/api/agent/:path*', // '/api/agent/:path*',
'/api/:path*', "/api/:path*",
"/dashboard/:path*",
"/dashboard",
], ],
}; };
-20
View File
@@ -1,20 +0,0 @@
// next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the session user type
};
}
interface User {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the user type
}
}
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
-20
View File
@@ -1,20 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
export default nextConfig;
+83
View File
@@ -0,0 +1,83 @@
import type {NextConfig} from "next";
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
function buildCSPHeader(): string {
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
const directives = [
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
`script-src ${CSP.SCRIPT_SRC.join(" ")}`,
`style-src ${CSP.STYLE_SRC.join(" ")}`,
`img-src ${CSP.IMG_SRC.join(" ")}`,
`font-src ${CSP.FONT_SRC.join(" ")}`,
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
`connect-src ${CSP.CONNECT_SRC.join(" ")}`,
`base-uri ${CSP.BASE_URI.join(" ")}`,
`form-action ${CSP.FORM_ACTION.join(" ")}`,
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
];
if (CSP.BLOCK_ALL_MIXED_CONTENT) {
directives.push("block-all-mixed-content");
}
if (CSP.UPGRADE_INSECURE_REQUESTS) {
directives.push("upgrade-insecure-requests");
}
return directives.join("; ");
}
function buildPermissionsPolicy(): string {
return Object.entries(PORTABASE_DEFAULT_SETTINGS.SECURITY.PERMISSIONS_POLICY)
.map(([feature, values]) => `${feature.toLowerCase()}=${values.join(", ")}`)
.join(", ");
}
const nextConfig: NextConfig = {
output: "standalone",
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: buildCSPHeader(),
},
{
key: "Permissions-Policy",
value: buildPermissionsPolicy(),
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
}
// ...other security headers
],
},
];
},
};
export default nextConfig;
+113 -104
View File
@@ -1,106 +1,115 @@
{ {
"name": "portabase", "name": "portabase",
"version": "0.1.0", "version": "1.1.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 80", "dev": "next dev --turbopack -p 8887",
"build": "next build", "build": "next build --experimental-build-mode compile",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"email": "email dev" "email": "email dev",
}, "db:generate": "drizzle-kit generate",
"dependencies": { "db:migrate": "drizzle-kit migrate",
"@auth/prisma-adapter": "^2.7.4", "db:drop": "drizzle-kit drop",
"@hookform/resolvers": "^3.9.1", "auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
"@prisma/client": "^6.1.0", },
"@radix-ui/react-accordion": "^1.2.1", "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.2", "@hookform/resolvers": "^5.0.1",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.6",
"@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-context-menu": "^2.2.2", "@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-context-menu": "^2.2.14",
"@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-menubar": "^1.1.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.1", "@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.1.3", "@radix-ui/react-toggle": "^1.1.8",
"@react-email/components": "^0.0.28", "@radix-ui/react-toggle-group": "^1.1.9",
"@t3-oss/env-nextjs": "^0.11.1", "@radix-ui/react-tooltip": "^1.2.6",
"@tanstack/react-query": "^5.62.11", "@react-email/components": "^0.0.41",
"@tanstack/react-table": "^8.20.5", "@t3-oss/env-nextjs": "^0.13.4",
"@types/ws": "^8.5.13", "@tanstack/react-query": "^5.76.1",
"@zenstackhq/runtime": "2.10.2", "@tanstack/react-table": "^8.21.3",
"argon2": "^0.41.1", "@types/nodemailer": "^6.4.17",
"bcrypt": "^5.1.1", "@types/ws": "^8.18.1",
"class-variance-authority": "^0.7.0", "@zenstackhq/runtime": "2.14.2",
"clsx": "^2.1.1", "argon2": "^0.43.0",
"cmdk": "^1.0.0", "bcrypt": "^6.0.0",
"date-fns": "^4.1.0", "better-auth": "1.3.1",
"dockerode": "^4.0.2", "class-variance-authority": "^0.7.1",
"embla-carousel-react": "^8.3.1", "clsx": "^2.1.1",
"input-otp": "^1.4.0", "cmdk": "^1.1.1",
"lucide-react": "^0.468.0", "date-fns": "^4.1.0",
"minio": "^8.0.2", "dockerode": "^4.0.6",
"next": "15.1.3", "dotenv": "^16.5.0",
"next-auth": "^5.0.0-beta.25", "drizzle-orm": "^0.43.1",
"next-safe-action": "^7.10.2", "drizzle-zod": "^0.7.1",
"next-themes": "^0.3.0", "embla-carousel-react": "^8.6.0",
"nodemailer": "^6.9.16", "input-otp": "^1.4.2",
"npm-check-updates": "^17.1.13", "lucide-react": "^0.510.0",
"react": "19.0.0", "minio": "^8.0.5",
"react-day-picker": "8.10.1", "next": "15.5.2",
"react-dom": "19.0.0", "next-safe-action": "^7.10.8",
"react-email": "^3.0.2", "next-themes": "^0.4.6",
"react-hook-form": "^7.53.1", "node-cron": "^4.2.1",
"react-resizable-panels": "^2.1.6", "nodemailer": "^7.0.3",
"react-twc": "^1.4.2", "npm-check-updates": "^18.0.1",
"recharts": "^2.15.0", "pg": "^8.16.0",
"socket.io": "^4.8.1", "react": "19.1.0",
"socket.io-client": "^4.8.1", "react-day-picker": "9.7.0",
"sonner": "^1.6.1", "react-dom": "19.1.0",
"tailwind-merge": "^2.5.4", "react-dropzone": "^14.3.8",
"tailwindcss-animate": "^1.0.7", "react-email": "^4.0.13",
"uuid": "^11.0.3", "react-hook-form": "^7.56.3",
"vaul": "^1.1.1", "react-resizable-panels": "^3.0.2",
"ws": "^8.18.0", "react-twc": "^1.4.2",
"zod": "^3.23.8" "recharts": "^2.15.3",
}, "socket.io": "^4.8.1",
"devDependencies": { "socket.io-client": "^4.8.1",
"@types/eslint-plugin-tailwindcss": "^3", "sonner": "^2.0.3",
"@types/node": "^20", "tailwind-merge": "^3.3.0",
"@types/pg": "^8", "uuid": "^11.1.0",
"@types/react": "npm:types-react@19.0.0-rc.1", "vaul": "^1.1.2",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "ws": "^8.18.2",
"@zenstackhq/openapi": "^2.10.2", "zod": "^3.24.4"
"@zenstackhq/tanstack-query": "^2.10.2", },
"eslint": "^8", "devDependencies": {
"eslint-config-next": "15.0.3", "@iconify/react": "^6.0.0",
"eslint-plugin-tailwindcss": "^3.17.5", "@tailwindcss/postcss": "^4.1.7",
"postcss": "^8", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"prisma": "^6.1.0", "@types/node": "^22.15.18",
"tailwindcss": "^3.4.1", "@types/pg": "^8.15.2",
"typescript": "^5", "@types/react": "^19.1.4",
"zenstack": "2.10.2" "@types/react-dom": "^19.1.5",
}, "@zenstackhq/openapi": "^2.14.2",
"overrides": { "@zenstackhq/tanstack-query": "^2.14.2",
"@types/react": "npm:types-react@19.0.0-rc.1", "drizzle-kit": "^0.31.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" "eslint": "^9.26.0",
} "eslint-config-next": "15.3.2",
"eslint-plugin-tailwindcss": "^3.18.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.7",
"tsx": "^4.19.4",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
"zenstack": "2.14.2"
},
"packageManager": "yarn@4.9.1"
} }
+62
View File
@@ -0,0 +1,62 @@
export const PORTABASE_DEFAULT_SETTINGS = {
SECURITY: {
CSP: {
DEFAULT_SRC: ["'self'"],
SCRIPT_SRC: [
"'self'",
"'unsafe-eval'",
"'unsafe-inline'",
"https://cdn.jsdelivr.net",
"https://www.googletagmanager.com",
"https://code.iconify.design",
"https://code.iconify.com",
"https://cdn.iconify.design",
"https://api.iconify.design",
],
STYLE_SRC: [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com",
"https://code.iconify.design",
"https://cdn.iconify.design",
"https://code.iconify.com",
],
IMG_SRC: [
"'self'",
"blob:",
"data:",
"https:",
"https://code.iconify.design",
"https://cdn.iconify.design",
"https://code.iconify.com",
"https://api.iconify.design",
"http://localhost:9000",
],
FONT_SRC: [
"'self'",
"https://fonts.gstatic.com",
"https://cdn.iconify.design",
],
CONNECT_SRC: [
"'self'",
"https://api.iconify.design",
"https://code.iconify.design",
],
OBJECT_SRC: ["'none'"],
BASE_URI: ["'self'"],
FORM_ACTION: ["'self'"],
FRAME_ANCESTORS: ["'none'"],
BLOCK_ALL_MIXED_CONTENT: false,
UPGRADE_INSECURE_REQUESTS: true,
},
PERMISSIONS_POLICY: {
CAMERA: ["()"],
MICROPHONE: ["()"],
GEOLOCATION: ["()"],
FULLSCREEN: ["(self)"],
// ...other features
},
},
};
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: { plugins: {
tailwindcss: {}, '@tailwindcss/postcss': {},
}, },
}; };
@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verificationtokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"role" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "password" TEXT;
@@ -1,46 +0,0 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('waiting', 'ongoing', 'failed', 'success');
-- CreateTable
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Database" (
"id" TEXT NOT NULL,
"agent_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Database_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Backup" (
"id" TEXT NOT NULL,
"database_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Backup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Restore" (
"id" TEXT NOT NULL,
"backup_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Restore_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "authMethod" TEXT;
@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Agent` will be added. If there are existing duplicate values, this will fail.
- Added the required column `slug` to the `Agent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Agent" ADD COLUMN "slug" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug");
-- AddForeignKey
ALTER TABLE "Database" ADD CONSTRAINT "Database_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,33 +0,0 @@
/*
Warnings:
- You are about to drop the column `authMethod` on the `users` table. All the data in the column will be lost.
- You are about to drop the `Restore` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "authMethod",
ADD COLUMN "auth_method" TEXT;
-- DropTable
DROP TABLE "Restore";
-- CreateTable
CREATE TABLE "Restauration" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "Restauration_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Backup" ADD CONSTRAINT "Backup_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "Backup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,13 +0,0 @@
-- CreateEnum
CREATE TYPE "TypeStorage" AS ENUM ('local', 's3');
-- CreateTable
CREATE TABLE "Settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
CONSTRAINT "Settings_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "s3EndPointUrl" TEXT;
@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "smtpFrom" TEXT,
ADD COLUMN "smtpHost" TEXT,
ADD COLUMN "smtpPassword" TEXT,
ADD COLUMN "smtpPort" TEXT,
ADD COLUMN "smtpUser" TEXT;
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `name` to the `Settings` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "name" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Settings` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Settings_name_key" ON "Settings"("name");
@@ -1,118 +0,0 @@
/*
Warnings:
- You are about to drop the `Agent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Backup` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Database` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Restauration` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Settings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Backup" DROP CONSTRAINT "Backup_database_id_fkey";
-- DropForeignKey
ALTER TABLE "Database" DROP CONSTRAINT "Database_agent_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_database_id_fkey";
-- DropTable
DROP TABLE "Agent";
-- DropTable
DROP TABLE "Backup";
-- DropTable
DROP TABLE "Database";
-- DropTable
DROP TABLE "Restauration";
-- DropTable
DROP TABLE "Settings";
-- CreateTable
CREATE TABLE "agents" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "databases" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"agent_id" TEXT NOT NULL,
CONSTRAINT "databases_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "backups" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"database_id" TEXT NOT NULL,
CONSTRAINT "backups_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "restaurations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restaurations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"name" TEXT NOT NULL,
"s3EndPointUrl" TEXT,
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
"smtpPassword" TEXT,
"smtpFrom" TEXT,
"smtpHost" TEXT,
"smtpPort" TEXT,
"smtpUser" TEXT,
CONSTRAINT "settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "agents_slug_key" ON "agents"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "settings_name_key" ON "settings"("name");
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;

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