mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
210
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98e40466a0 | ||
|
|
324d80a735 | ||
|
|
6770be63b6 | ||
|
|
89aa33ebd8 | ||
|
|
619c32f09d | ||
|
|
c471423775 | ||
|
|
c589b9b8b8 | ||
|
|
bae9ba0973 | ||
|
|
7bfc5c1d9b | ||
|
|
8bd3b0d731 | ||
|
|
06cd4de141 | ||
|
|
33eb2df023 | ||
|
|
540ecd4b60 | ||
|
|
237418f009 | ||
|
|
4447180e9e | ||
|
|
f111946592 | ||
|
|
475e404fae | ||
|
|
07ce5215d4 | ||
|
|
9f4895fd63 | ||
|
|
c18a9da0d3 | ||
|
|
8e3eb32631 | ||
|
|
f0598b3f59 | ||
|
|
011120fdf9 | ||
|
|
ff175e1edd | ||
|
|
3f56008c39 | ||
|
|
5df95bf7c1 | ||
|
|
23d52d0447 | ||
|
|
795a127998 | ||
|
|
fd73d383f9 | ||
|
|
ee285df7b5 | ||
|
|
7f7f8e642f | ||
|
|
d9997ff5e9 | ||
|
|
36adcb323a | ||
|
|
5302060f44 | ||
|
|
d19a46bee6 | ||
|
|
d504866aad | ||
|
|
601dae4909 | ||
|
|
08f2ad0521 | ||
|
|
3b356e33e7 | ||
|
|
57d9b8da1e | ||
|
|
86f50e5741 | ||
|
|
a6e52fd03d | ||
|
|
116229a29e | ||
|
|
428782e8a4 | ||
|
|
b8442803d9 | ||
|
|
82ab2c63d6 | ||
|
|
e953de0e06 | ||
|
|
f6d927f866 | ||
|
|
3773c4b068 | ||
|
|
bb0dbdadae | ||
|
|
dd8aec50eb | ||
|
|
7410aaea38 | ||
|
|
0f9bbbda3c | ||
|
|
25f9bc6e79 | ||
|
|
caf00b7877 | ||
|
|
9c0c462192 | ||
|
|
ef7570db76 | ||
|
|
e1bc1ec6c8 | ||
|
|
ca232250cc | ||
|
|
fde7688485 | ||
|
|
9458b12d69 | ||
|
|
5cf6cf075e | ||
|
|
8980e6d4a8 | ||
|
|
ac951378e8 | ||
|
|
934e635cdb | ||
|
|
d968aa9c16 | ||
|
|
eb471c7cdb | ||
|
|
e886bdd58d | ||
|
|
fe5d8bae1b | ||
|
|
aabdea06a9 | ||
|
|
37a3ace161 | ||
|
|
933ab7d68a | ||
|
|
f42cb37d67 | ||
|
|
7dfd9f02e8 | ||
|
|
411e09f452 | ||
|
|
f21149f42e | ||
|
|
099b6fb0e8 | ||
|
|
eb2e0ef468 | ||
|
|
4644ed2f41 | ||
|
|
cf8e5b7094 | ||
|
|
b0775e82ea | ||
|
|
f9b3461a44 | ||
|
|
c4ebc876bf | ||
|
|
a435a92d41 | ||
|
|
bcbd6bbea1 | ||
|
|
82559b49bd | ||
|
|
8c1b869004 | ||
|
|
c4a0692985 | ||
|
|
750f1cb145 | ||
|
|
b7d3487c53 | ||
|
|
529f157c6e | ||
|
|
c62be9854a | ||
|
|
9c1aa66b41 | ||
|
|
9894e2582d | ||
|
|
8a8ef81fce | ||
|
|
2fdd9f1296 | ||
|
|
5251e01011 | ||
|
|
84dab35f4b | ||
|
|
ae766dab2f | ||
|
|
027928c29f | ||
|
|
e3239639f3 | ||
|
|
7d6038e5f1 | ||
|
|
2a90d4933d | ||
|
|
1fbd979d40 | ||
|
|
4e06d2bf97 | ||
|
|
8ce3988613 | ||
|
|
b7f161d200 | ||
|
|
a3f5c4e494 | ||
|
|
f74f378c0b | ||
|
|
385836d5fe | ||
|
|
461e3ff7c9 | ||
|
|
54a233b207 | ||
|
|
2d2453acf7 | ||
|
|
654ff6b9a5 | ||
|
|
34ff11f377 | ||
|
|
b95e145a8b | ||
|
|
bce6c9d55f | ||
|
|
c3e2978919 | ||
|
|
2d559e456d | ||
|
|
45e3b31fda | ||
|
|
c8da2579e8 | ||
|
|
c7e30e1659 | ||
|
|
7be23876aa | ||
|
|
98f1b329ae | ||
|
|
19ea5543ea | ||
|
|
010396ba4d | ||
|
|
f055744432 | ||
|
|
1920b29a55 | ||
|
|
a0c8d1fb68 | ||
|
|
59dc3b71b8 | ||
|
|
598eb44952 | ||
|
|
ac49047177 | ||
|
|
4448c9e166 | ||
|
|
d70778b556 | ||
|
|
66edb77113 | ||
|
|
0f7b3eae11 | ||
|
|
2f9e33f4db | ||
|
|
dd7a65262c | ||
|
|
4c0d2b1f7f | ||
|
|
60728885b6 | ||
|
|
7ac99a6618 | ||
|
|
4ed91c1297 | ||
|
|
44be8009fd | ||
|
|
96630a0ef7 | ||
|
|
8c9d694b40 | ||
|
|
1f09c1507a | ||
|
|
141241d555 | ||
|
|
c605e8173b | ||
|
|
37eb8f319f | ||
|
|
0492d244b3 | ||
|
|
baeec9f7a0 | ||
|
|
449c9c2cfe | ||
|
|
d13e4b7d40 | ||
|
|
13241811d0 | ||
|
|
4b52dc4a1e | ||
|
|
63cb6cb7a3 | ||
|
|
0511ad289c | ||
|
|
057a51499e | ||
|
|
1f01d9c372 | ||
|
|
0684a4887a | ||
|
|
96be14e1ba | ||
|
|
eec3c2737b | ||
|
|
a6b960ffb2 | ||
|
|
d3f465b5d4 | ||
|
|
2139dfd55d | ||
|
|
9f53181467 | ||
|
|
c96e13c72f | ||
|
|
58d9179152 | ||
|
|
17469696ee | ||
|
|
85047e7509 | ||
|
|
f9dc15ba73 | ||
|
|
7be542f676 | ||
|
|
847a567c52 | ||
|
|
1ffbd6efc8 | ||
|
|
f8f599e4dd | ||
|
|
f7cd169090 | ||
|
|
bb4003b0db | ||
|
|
159398465f | ||
|
|
76e5e55f14 | ||
|
|
605946abaa | ||
|
|
2890d6ac40 | ||
|
|
ac3b4e424a | ||
|
|
8154f85eb6 | ||
|
|
45312207c9 | ||
|
|
128792645b | ||
|
|
4d52419247 | ||
|
|
7abdcd57f9 | ||
|
|
5e704ec18a | ||
|
|
de9fe33194 | ||
|
|
385777535f | ||
|
|
405de7b323 | ||
|
|
6f2523e524 | ||
|
|
01019fe1a3 | ||
|
|
b1ecb7ce01 | ||
|
|
0cf350cdcc | ||
|
|
ed5f7a43f2 | ||
|
|
54f0e87805 | ||
|
|
f570a67805 | ||
|
|
d0ec5902b5 | ||
|
|
8586abf7e5 | ||
|
|
6562718635 | ||
|
|
7a59c616eb | ||
|
|
2440592286 | ||
|
|
dca1799aac | ||
|
|
d42d73661e | ||
|
|
a43878688b | ||
|
|
7ca905440c | ||
|
|
c733fb4fdc | ||
|
|
7193f5b71f | ||
|
|
b59a010425 |
+22
-1
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Environnement
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Base de données
|
||||||
|
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||||
|
|
||||||
|
# Projet
|
||||||
|
PROJECT_NAME="Portabase"
|
||||||
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
|
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=
|
||||||
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
@@ -26,7 +26,7 @@ Please take a moment to review this guide. It will help you understand how to co
|
|||||||
|
|
||||||
2. **Clone the repository**
|
2. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
git clone https://github.com/Portabase/portabase
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up the development environment**
|
3. **Set up the development environment**
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
We take security seriously and aim to support the following versions of the project with security updates:
|
We take security seriously and aim to support the following versions of the project with security updates:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|-----------|--------------------|
|
|---------|--------------------|
|
||||||
| 1.x | ✅ Fully Supported |
|
| Latest | ✅ Fully Supported |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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,14 +46,5 @@ 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
|
||||||
|
|
||||||
|
|
||||||
# - name: Generate artifact attestation
|
|
||||||
# uses: actions/attest-build-provenance@v2
|
|
||||||
# with:
|
|
||||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
|
||||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
# push-to-registry: true
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
|
with:
|
||||||
|
config-path: .gitleaks.toml # Optional, if you have a custom config
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
.idea
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.yarn/install-state.gz
|
#.yarn/install-state.gz
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
title = "Custom gitleaks config"
|
||||||
|
|
||||||
|
[allowlist]
|
||||||
|
# Global allowlist patterns (won’t be flagged)
|
||||||
|
regexes = [
|
||||||
|
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
|
||||||
|
]
|
||||||
|
paths = [
|
||||||
|
"src/utils/init.ts"
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.24.2
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"WillLuke.nextjs.hasPrompted": true
|
||||||
|
}
|
||||||
Binary file not shown.
Vendored
-934
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
nodeLinker: "node-modules"
|
nodeLinker: node-modules
|
||||||
yarnPath: .yarn/releases/yarn-4.5.3.cjs
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
title: Portabase
|
||||||
|
message: "If you use this software, please cite it as below."
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- family-names: Gauthereau
|
||||||
|
given-names: Charles
|
||||||
|
- family-names: Larcher
|
||||||
|
given-names: Killian
|
||||||
|
repository-code: https://github.com/Portabase/portabase
|
||||||
|
url: https://portabase.io
|
||||||
|
abstract: "Portabase is a free, open-source, self-hosted solution for database administration, providing backup and restore capabilities, scheduling, retention policies, notifications, and support for multiple storage backends. Its headless agent architecture enables connection to multiple database instances securely and efficiently."
|
||||||
|
keywords:
|
||||||
|
- docker
|
||||||
|
- kubernetes
|
||||||
|
- backups
|
||||||
|
- postgresql
|
||||||
|
- mysql
|
||||||
|
- mariadb
|
||||||
|
- devops
|
||||||
|
- database
|
||||||
|
- monitoring
|
||||||
|
- s3
|
||||||
|
- self-hosted
|
||||||
|
- system-administration
|
||||||
|
- web-ui
|
||||||
|
- agent
|
||||||
|
license: Apache-2.0
|
||||||
|
version: 1.1.6
|
||||||
|
date-released: "2026-01-01"
|
||||||
@@ -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)
|
||||||
@@ -1,176 +1,316 @@
|
|||||||
<!-- PROJECT LOGO -->
|
<br />
|
||||||
|
<div align="center">
|
||||||
[//]: # (<br />)
|
<a href="https://portabase.io">
|
||||||
|
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
|
||||||
[//]: # (<div align="center">)
|
</a>
|
||||||
|
|
||||||
[//]: # ( <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>
|
<h3 align="center">Portabase</h3>
|
||||||
|
<p>
|
||||||
<p align="center">
|
Take full control of your databases with Portabase — the self-hosted, open-source platform
|
||||||
Backup / Restore your databases instances
|
for automated backup, restoration, and operational management. Powered by the <strong>Portabase Agent</strong>,
|
||||||
<br />
|
every database in your infrastructure can be monitored, backed up, and managed in real time,
|
||||||
<a href="https://portabase.io"><strong>Explore the docs »</strong></a>
|
with zero reliance on third-party services.
|
||||||
<br />
|
</p>
|
||||||
<br />
|
<p>
|
||||||
<a href="https://portabase.io">View Demo</a>
|
Secure, lightweight, and deployable anywhere — on Docker, Kubernetes, or directly on your servers.
|
||||||
·
|
Designed for teams, DevOps, and enterprises who demand control, reliability, and automation at scale.
|
||||||
<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>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
[](https://www.postgresql.org/)
|
||||||
|
[](https://www.mysql.com/)
|
||||||
|
[](https://mariadb.org/)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
|
||||||
<!-- TABLE OF CONTENTS -->
|
<p>
|
||||||
<details>
|
<strong>
|
||||||
<summary>Table of Contents</summary>
|
<a href="https://portabase.io">Documentation</a> •
|
||||||
<ol>
|
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
|
||||||
<li>
|
<a href="#installation">Installation</a> •
|
||||||
<a href="#about-the-project">About The Project</a>
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
<ul>
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
<li><a href="#built-with">Built With</a></li>
|
</strong>
|
||||||
</ul>
|
</p>
|
||||||
</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>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
</div>
|
||||||
## About The Project
|
|
||||||
|
|
||||||
Portabase is the server dashboard service working with portabase agents.
|
---
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
## ✨ About The Project
|
||||||
|
|
||||||
|
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It
|
||||||
|
integrates seamlessly with [Portabase agents](https://github.com/Portabase/agent-portabase) for managing operations
|
||||||
|
securely and efficiently.
|
||||||
|
|
||||||
|
### 🔧 Built With
|
||||||
|
|
||||||
### Built With
|
- [![NextJS][NextJS]][NextJS-url]
|
||||||
|
- [![Drizzle][Drizzle]][Drizzle-url]
|
||||||
|
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||||
|
- [![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
|
- [![Docker][Docker]][Docker-url]
|
||||||
|
|
||||||
* [![NextJS][NextJS]][NextJS-url]
|
---
|
||||||
* [![Prisma][Prisma]][Prisma-url]
|
|
||||||
|
|
||||||
|
## 📦 Features
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
### 🗄️ Supported databases
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- MySQL
|
||||||
|
- MariaDB
|
||||||
|
|
||||||
|
### ⏱️ Scheduled backups
|
||||||
|
|
||||||
<!-- GETTING STARTED -->
|
- Cron-based scheduling for full control
|
||||||
## Getting Started
|
- Manual trigger support for on-demand backups
|
||||||
|
|
||||||
### Prerequisites
|
### 💾 Storage backends
|
||||||
|
|
||||||
Install docker on your machine
|
- On-premise storage: Backups are stored directly on your VPS or server
|
||||||
|
- Cloud storage: S3-compatible backends
|
||||||
|
supported ([documentation](https://portabase.io/docs/dashboard/advanced/storage/s3))
|
||||||
|
- Full data ownership: No third-party access — your data stays under your control
|
||||||
|
|
||||||
|
### 🔔 Smart notifications
|
||||||
|
|
||||||
|
- Multi-channel delivery: Email, Slack, Discord, webhooks
|
||||||
|
- Real-time alerts: Immediate feedback on success and failure
|
||||||
|
- Custom alert policies: Database-level notification rules
|
||||||
|
- Team-ready: Designed for DevOps, on-call, and incident workflows
|
||||||
|
|
||||||
|
### 👥 Built for team environments
|
||||||
|
|
||||||
|
- Workspaces: Organize databases, notification channels, and storage backends by organization and project
|
||||||
|
- Access control: Fine-grained, role-based permissions on all resources
|
||||||
|
- Role management: Member, admin, and owner roles at both system and organization levels
|
||||||
|
|
||||||
|
### 🐳 Self-hosted & secure
|
||||||
|
|
||||||
|
- Containerized deployment: Docker-based setup for predictable installation and operations
|
||||||
|
- Privacy by design: All data remains within your own infrastructure
|
||||||
|
- Open source: Apache 2.0 licensed — fully auditable codebase
|
||||||
|
|
||||||
|
### 🤖 Portabase Agent ([details](https://github.com/Portabase/agent-portabase))
|
||||||
|
|
||||||
|
- Headless architecture: Runs locally on your infrastructure to manage backups and database operations
|
||||||
|
- Multi-target support: Single agent can connect to multiple databases across different servers
|
||||||
|
- Lightweight & efficient: Minimal resource footprint while providing full operational control
|
||||||
|
- Secure communication: Encrypted channels between agent and central dashboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Installation steps in order to use it locally and work on some features improvements
|
You have 3 ways to install **Portabase**:
|
||||||
|
|
||||||
1. Clone the repo
|
- Automated CLI (recommended) - [details](https://portabase.io/docs/cli)
|
||||||
```sh
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup)
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
- Kubernetes with Helm (soon)
|
||||||
```
|
|
||||||
3. Start the project
|
|
||||||
```sh
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
Ensure Docker is installed on your machine before getting started.
|
||||||
|
|
||||||
|
### Docker Compose Setup
|
||||||
|
|
||||||
|
Create a `docker-compose.yml` file with the following configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: portabase
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
portabase:
|
||||||
|
image: solucetechnologies/portabase:latest
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- '8887:80'
|
||||||
|
environment:
|
||||||
|
- TZ="Europe/Paris"
|
||||||
|
volumes:
|
||||||
|
- portabase-private:/app/private
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
container_name: portabase-app
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=<your_database>
|
||||||
|
- POSTGRES_USER=<database_user>
|
||||||
|
- POSTGRES_PASSWORD=<database_password>
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U <database_user> -d <your_database>" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
portabase-private:
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use reverse proxy like
|
||||||
|
Traefik : [Check this link](https://portabase.io/docs/dashboard/advanced/reverse-proxy)
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||||
|
|
||||||
|
# Project Info
|
||||||
|
PROJECT_NAME="Portabase"
|
||||||
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
|
PROJECT_URL=http://app.portabase.io
|
||||||
|
PROJECT_SECRET=
|
||||||
|
|
||||||
|
# SMTP (Email)
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_FROM=
|
||||||
|
|
||||||
|
# Google OAuth
|
||||||
|
AUTH_GOOGLE_ID=
|
||||||
|
AUTH_GOOGLE_SECRET=
|
||||||
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Storage Backend: 'local' or 's3'
|
||||||
|
STORAGE_TYPE=local
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
|
```
|
||||||
|
|
||||||
|
To get more information about env variables, check
|
||||||
|
that [link](https://portabase.io/docs/dashboard/advanced/environment)
|
||||||
|
|
||||||
|
### Locally (Development)
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Portabase/portabase
|
||||||
|
cd portabase
|
||||||
|
```
|
||||||
|
2. Start the development database for Portabase service:
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the Next.js app:
|
||||||
|
```bash
|
||||||
|
make up
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Usage
|
||||||
|
|
||||||
|
Once the installation process is done, follow the steps to configure your instance.
|
||||||
|
|
||||||
|
### Dashboard configuration process
|
||||||
|
|
||||||
|
1. **Access the dashboard** – Open `http://localhost:8887` in your browser.
|
||||||
|
2. **Sign up** – Register the first user, who will automatically have the **Admin** role in the default workspace.
|
||||||
|
3. **Add your first agent** – Follow [this guide](https://github.com/Portabase/agent-portabase) for setup
|
||||||
|
instructions.
|
||||||
|
4. **Create organizations and projects** – Link your databases to projects to enable backups and restores.
|
||||||
|
5. **Configure backup policies** – Define schedules (hourly, daily, weekly, or monthly) and retention rules.
|
||||||
|
6. **Choose a storage provider** – Select where backups will be stored (local, S3, etc.).
|
||||||
|
7. **Save and start** – Portabase validates your configuration and starts automated backups based on your defined
|
||||||
|
policies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome and appreciated! Here's how to get started:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a new branch:
|
||||||
|
```bash
|
||||||
|
git checkout -b features/your-feature
|
||||||
|
```
|
||||||
|
3. Commit your changes:
|
||||||
|
```bash
|
||||||
|
git commit -m "Add YourFeature"
|
||||||
|
```
|
||||||
|
4. Push to the branch:
|
||||||
|
```bash
|
||||||
|
git push origin features/your-feature
|
||||||
|
```
|
||||||
|
5. Open a pull request
|
||||||
|
|
||||||
|
### Top Contributors
|
||||||
|
|
||||||
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
Thanks to all contributors and the open-source community!
|
||||||
|
|
||||||
|
Give the project a ⭐ if you like it!
|
||||||
|
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] Add Changelog
|
|
||||||
- [ ] Improve documentation and repositories
|
|
||||||
- [ ] Add tests procedure
|
|
||||||
- [ ] Add Release File
|
|
||||||
- [ ] Security improvements
|
|
||||||
- [ ] Multi locale support
|
|
||||||
- [ ] Multi db support
|
|
||||||
- [x] PostgreSQL
|
|
||||||
- [ ] MongoDB
|
|
||||||
- [ ] MySQL
|
|
||||||
|
|
||||||
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues).
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
||||||
|
|
||||||
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".
|
|
||||||
Don't forget to give the project a star! Thanks again!
|
|
||||||
|
|
||||||
1. Fork the Project
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|
||||||
## Memo
|
|
||||||
|
|
||||||
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format:
|
|
||||||
|
|
||||||
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)```
|
|
||||||
|
|
||||||
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)```
|
|
||||||
|
|
||||||
|
|
||||||
<!-- LICENSE -->
|
|
||||||
## License
|
|
||||||
|
|
||||||
Distributed under the Apache License. See `LICENSE.txt` for more information.
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTACT -->
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
Killian Larcher - killian.larcher@soluce-technologies.com
|
|
||||||
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
|
||||||
|
|
||||||
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase)
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
<!-- MARKDOWN LINKS & IMAGES -->
|
|
||||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
||||||
|
|
||||||
[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
|
|
||||||
[Prisma-url]: https://www.prisma.io/
|
[BetterAuth]: https://img.shields.io/badge/Better%20Auth-FFF?logo=betterauth&logoColor=000&style=for-the-badge
|
||||||
|
|
||||||
|
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
|
||||||
|
|
||||||
|
[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/
|
||||||
|
|
||||||
|
[BetterAuth-url]: https://www.better-auth.com/
|
||||||
|
|
||||||
|
[Drizzle-url]: https://orm.drizzle.team/
|
||||||
|
|
||||||
|
[ShadcnUI-url]: https://ui.shadcn.com/
|
||||||
|
|
||||||
|
[Docker-url]: https://www.docker.com/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {ForgotPasswordForm} from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Reset password</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a
|
||||||
|
link to reset your password.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ForgotPasswordForm/>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {GuardForm} from "@/components/wrappers/auth/guard/guard-form";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {redirect} from "next/navigation";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
|
export default async function GuardPage() {
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("better-auth.two_factor")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Two-factor verification</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Please enter the verification code generated
|
||||||
|
by your authentication app.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<GuardForm/>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+14
-23
@@ -1,34 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {LayoutAdmin} from "@/components/layout";
|
|
||||||
import {currentUser} from "@/auth/current-user";
|
|
||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
import {prisma} from "@/prisma";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
|
||||||
|
|
||||||
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){
|
|
||||||
const userInfo = await prisma.user.findUnique({
|
if (user && !user.banned && user.role !== "pending") {
|
||||||
where: {
|
redirect("/dashboard/home");
|
||||||
email: user?.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if(userInfo && userInfo.authMethod){
|
|
||||||
redirect('/')
|
|
||||||
// redirect('/dashboard')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 "
|
<AuthLogoSection/>
|
||||||
>
|
<div>{children}</div>
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
{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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+46
-23
@@ -1,29 +1,52 @@
|
|||||||
"use client"
|
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
|
||||||
|
import {SocialAuthButtons} from "@/components/wrappers/auth/social-buttons";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
import {useEffect, useRef} from "react";
|
export const metadata: Metadata = {
|
||||||
import {useSearchParams} from "next/navigation";
|
title: "Login",
|
||||||
|
};
|
||||||
|
|
||||||
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm";
|
export default async function SignInPage() {
|
||||||
import {toast} from "sonner";
|
|
||||||
|
|
||||||
export default function SignInPage(props: {
|
|
||||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const error = searchParams.get("error");
|
|
||||||
const isFirstRender = useRef(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstRender.current && error) {
|
|
||||||
toast.error("Error occurred.");
|
|
||||||
isFirstRender.current = false;
|
|
||||||
}
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-[350px] gap-6">
|
<TooltipProvider>
|
||||||
<LoginForm/>
|
<CardAuth className="w-full">
|
||||||
</div>
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Login</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Fill your login informations</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<LoginForm/>
|
||||||
|
|
||||||
|
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||||
|
<Separator/>
|
||||||
|
<div className="px-2 text-center text-sm">OR</div>
|
||||||
|
<Separator/>
|
||||||
|
</div>
|
||||||
|
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm">
|
||||||
|
Don't have an account ?{" "}
|
||||||
|
<Link href="/register" className="underline">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
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";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Register",
|
||||||
|
};
|
||||||
|
|
||||||
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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { auth } from "@/lib/auth/auth";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||||
|
|
||||||
|
const { token } = await props.searchParams;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${token}`);
|
||||||
|
|
||||||
|
if (!verification || verification.expiresAt < new Date()) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Card className="w-full max-w-md shadow-lg">
|
||||||
|
<CardHeader className="space-y-4">
|
||||||
|
<div className="space-y-1 text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
||||||
|
<p className="text-sm text-muted-foreground text-balance">Please enter your new password below.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center space-y-2 text-center">
|
||||||
|
<Avatar className="relative flex h-16 w-16 shrink-0 overflow-hidden rounded-full border">
|
||||||
|
<AvatarImage src={user!.image ?? ""} alt={user!.name} className="aspect-square h-full w-full object-cover" />
|
||||||
|
<AvatarFallback className="flex h-full w-full items-center justify-center rounded-full bg-muted text-2xl font-medium text-muted-foreground">
|
||||||
|
{user!.name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="font-semibold text-lg">{user!.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{user!.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page} from "@/features/layout/page";
|
||||||
|
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
|
||||||
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
|
import {isUUID} from "@/utils/text";
|
||||||
|
import {user} from "@/db/schema/02_user";
|
||||||
|
import {invitation} from "@/db/schema/05_invitation";
|
||||||
|
import {member} from "@/db/schema/04_member";
|
||||||
|
import {organization} from "@/db/schema/03_organization";
|
||||||
|
import {user as drizzleUser} from "@/db/schema/02_user";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{ organizationId: string }>) {
|
||||||
|
const {organizationId} = await props.params;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUUID(organizationId)) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await db.select().from(drizzleUser);
|
||||||
|
|
||||||
|
const organizationData = await db
|
||||||
|
.select({organization, member, user, invitation})
|
||||||
|
.from(organization)
|
||||||
|
.leftJoin(member, eq(drizzleDb.schemas.organization.id, member.organizationId))
|
||||||
|
.leftJoin(invitation, eq(drizzleDb.schemas.invitation.id, invitation.organizationId))
|
||||||
|
.leftJoin(user, eq(drizzleDb.schemas.member.userId, user.id))
|
||||||
|
.where(eq(organization.id, organizationId));
|
||||||
|
|
||||||
|
const formattedData = buildOrganizationWithMembers(organizationData);
|
||||||
|
|
||||||
|
if (!formattedData) return notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<OrganizationManagement organization={formattedData} users={users}/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {
|
||||||
|
AdminOrganizationAddModal
|
||||||
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||||
|
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active organizations</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminOrganizationAddModal/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminOrganizationList organizations={organizations}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,42 +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 {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
|
||||||
where: {
|
|
||||||
// id: {
|
|
||||||
// not: user.id
|
|
||||||
// },
|
|
||||||
deleted: {not: true},
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const settings = await prisma.settings.findUnique({
|
|
||||||
where: {
|
|
||||||
name: "system"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Administration Panel
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="mt-10">
|
|
||||||
<AdminTabs settings={settings} currentUser={user} users={users}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">System email</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsEmailSection settings={settings}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">System storage</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsStorageSection settings={settings}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
||||||
|
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
accounts: true
|
||||||
|
},
|
||||||
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
|
||||||
|
});
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active users</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminUserAddModal organizations={organizations}/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminUserList users={users}/>
|
||||||
|
</PageContent>
|
||||||
|
</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,100 +1,58 @@
|
|||||||
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 {buttonVariants} from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {db} from "@/db";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import * as drizzleDb from "@/db";
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
import {eq} from "drizzle-orm";
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
|
import {notFound} from "next/navigation";
|
||||||
|
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
||||||
|
|
||||||
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';
|
if (!agent) {
|
||||||
|
notFound()
|
||||||
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
|
|
||||||
|
|
||||||
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
|
|
||||||
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>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Backups
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent></CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Success rate
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{successRate ?? "Unavailable for now."}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Last contact
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{formatDateLastContact(agent.lastContact)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</div>
|
{agent.description && (
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||||
<CardHeader className="font-bold text-xl">
|
)}
|
||||||
Edge Key
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
</CardHeader>
|
<AgentContentPage
|
||||||
<CardContent>
|
agent={agent}
|
||||||
<AgentCardKey agent={agent}/>
|
edgeKey={edgeKey}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
|
||||||
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,28 +1,21 @@
|
|||||||
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 {Metadata} from "next";
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Agent",
|
||||||
|
};
|
||||||
|
|
||||||
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,41 @@
|
|||||||
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 {desc, eq, not} from "drizzle-orm";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Agents",
|
||||||
|
};
|
||||||
|
|
||||||
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)),
|
||||||
|
with: {
|
||||||
|
databases: true
|
||||||
|
},
|
||||||
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(agents);
|
||||||
|
|
||||||
|
|
||||||
|
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 +44,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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -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,46 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {
|
||||||
|
NotificationChannelsSection
|
||||||
|
} from "@/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Notification Channels",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationChannels = await db.query.notificationChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
orderBy: desc(notificationChannel.createdAt)
|
||||||
|
}) as NotificationChannelWith[]
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Notification channels</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<NotifierAddEditModal adminView={false}/>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Activity logs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationLogs = await getNotificationHistory()
|
||||||
|
|
||||||
|
if (!notificationLogs) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Activity logs</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<NotificationLogsList notificationLogs={notificationLogs} />
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
+36
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {notFound, redirect} from "next/navigation";
|
||||||
|
import {Page, 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 {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 {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
|
||||||
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
|
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
projectId: string;
|
||||||
|
databaseId: string
|
||||||
|
}>) {
|
||||||
|
const {projectId, databaseId} = await props.params;
|
||||||
|
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
|
if (!organization || !activeMember) {
|
||||||
|
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,
|
||||||
|
alertPolicies: 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 organizationChannels = await getOrganizationChannels(organization.id);
|
||||||
|
const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
|
||||||
|
|
||||||
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
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="min-w-full md:min-w-fit ">
|
||||||
|
{capitalizeFirstLetter(dbItem.name)}
|
||||||
|
</div>
|
||||||
|
{!isMember && (
|
||||||
|
<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}/>
|
||||||
|
<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}
|
||||||
|
organizationId={organization.id}/>
|
||||||
|
<ImportModal 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 activeMember={activeMember} 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,94 @@
|
|||||||
|
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 {getActiveMember, 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({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="justify-between gap-2 sm:flex">
|
||||||
|
<PageTitle className="flex items-center">
|
||||||
|
{capitalizeFirstLetter(proj.name)}
|
||||||
|
{!isMember && (
|
||||||
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
|
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</PageTitle>
|
||||||
|
{!isMember && (
|
||||||
|
<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 haven’t added any databases to this project yet.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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,70 @@
|
|||||||
|
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 {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Projects</PageTitle>
|
||||||
|
{(projects.length > 0 && !isMember) && (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
) : isMember ? (
|
||||||
|
<EmptyStatePlaceholder text="No project available"/>
|
||||||
|
) : (
|
||||||
|
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||||
|
)}
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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,58 @@
|
|||||||
|
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 {Metadata} from "next";
|
||||||
|
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Settings",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
|
const organization = await getOrganization({});
|
||||||
|
const user = await currentUser();
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
|
if (!organization || !activeMember) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||||
|
|
||||||
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle className="flex items-center">
|
||||||
|
Organization settings
|
||||||
|
{permissions.canManageSettings && organization.slug !== "default" && (
|
||||||
|
<EditButtonSettings/>
|
||||||
|
)}
|
||||||
|
</PageTitle>
|
||||||
|
|
||||||
|
<PageActions>
|
||||||
|
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
||||||
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
|
)}
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<OrganizationTabs
|
||||||
|
activeMember={activeMember}
|
||||||
|
organization={organization}
|
||||||
|
notificationChannels={notificationChannels}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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 {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Statistics",
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 Overview</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 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||||
|
<Building2 className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{projectsCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||||
|
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{restorationsCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<EvolutionLineChart
|
||||||
|
data={sortedBackupsEvolution}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PercentageLineChart
|
||||||
|
data={backupsRate}/>
|
||||||
|
</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>
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
-39
@@ -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,76 +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,
|
|
||||||
},
|
|
||||||
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,79 +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: {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Home",
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,45 +1,26 @@
|
|||||||
import React from "react";
|
import React, {ReactNode} 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";
|
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
|
||||||
|
|
||||||
|
export default async function Layout({children}: { children: ReactNode }) {
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
const user = await currentUser();
|
||||||
|
if (!user) redirect("/login");
|
||||||
const user = await currentUser()
|
|
||||||
if (user) {
|
|
||||||
const userInfo = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: user?.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!userInfo) redirect('/login')
|
|
||||||
}
|
|
||||||
if (!user) redirect('/login')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <HydrationZustand>
|
<SidebarProvider>
|
||||||
// <GlobalStoreProvider>
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<>
|
<ThemeMetaUpdater/>
|
||||||
|
<AppSidebar/>
|
||||||
<SidebarProvider>
|
<SidebarInset>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<Header/>
|
||||||
<AppSidebar/>
|
<main className="h-full">{children}</main>
|
||||||
<SidebarInset>
|
</SidebarInset>
|
||||||
<Header/>
|
</div>
|
||||||
<main className="h-full">
|
</SidebarProvider>
|
||||||
{children}
|
);
|
||||||
</main>
|
|
||||||
</SidebarInset>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
</>
|
|
||||||
// </GlobalStoreProvider>
|
|
||||||
// </HydrationZustand>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {Badge} from "@/components/ui/badge";
|
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
if (!user) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
const userInfo = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: user.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="justify-between gap-2 sm:flex">
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
<AvatarWithUpload user={userInfo}/>
|
|
||||||
{user.name}
|
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
|
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
|
|
||||||
</PageTitle>
|
|
||||||
<PageActions className="mt-2 hidden sm:block">
|
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
|
||||||
<PageContent>
|
|
||||||
<UserForm userId={userInfo.id} defaultValues={userInfo}/>
|
|
||||||
<div className="mt-4 sm:hidden">
|
|
||||||
<ButtonDeleteAccount text="Delete my account" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/>
|
||||||
|
|||||||
@@ -1,2 +1,56 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import forge from "node-forge";
|
||||||
|
import {EventPayload} from "@/features/notifications/types";
|
||||||
|
import {dispatchNotification} from "@/features/notifications/dispatch";
|
||||||
|
import {Database, DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
|
|
||||||
|
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> {
|
||||||
|
const privateKeyPem = fs.readFileSync("private/keys/server_private.pem", "utf8");
|
||||||
|
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
||||||
|
|
||||||
|
// Decrypt AES key with RSA-OAEP
|
||||||
|
const encryptedAesKey = forge.util.hexToBytes(aesKeyHex);
|
||||||
|
const aesKey = privateKey.decrypt(encryptedAesKey, "RSA-OAEP", {
|
||||||
|
md: forge.md.sha256.create(),
|
||||||
|
mgf1: {md: forge.md.sha256.create()},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read encrypted file content
|
||||||
|
const encryptedBuffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
const iv = forge.util.hexToBytes(ivHex);
|
||||||
|
|
||||||
|
// AES decryption
|
||||||
|
const decipher = forge.cipher.createDecipher("AES-CBC", aesKey);
|
||||||
|
decipher.start({iv});
|
||||||
|
decipher.update(forge.util.createBuffer(encryptedBuffer.toString("binary")));
|
||||||
|
const success = decipher.finish();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error("Decryption failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedBytes = decipher.output.getBytes();
|
||||||
|
const decryptedBuffer = Buffer.from(decryptedBytes, "binary");
|
||||||
|
|
||||||
|
// Return a File so you can use file.arrayBuffer() later
|
||||||
|
return new File(
|
||||||
|
[decryptedBuffer],
|
||||||
|
file.name.replace(/\.enc$/, fileExtension),
|
||||||
|
{type: "application/octet-stream"}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getFileExtension(dbType: string) {
|
||||||
|
switch (dbType) {
|
||||||
|
case "postgresql":
|
||||||
|
return ".dump";
|
||||||
|
case "mysql":
|
||||||
|
return ".sql";
|
||||||
|
default:
|
||||||
|
return ".dump";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +1,103 @@
|
|||||||
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 * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {Backup} from "@/db/schema/07_database";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {decryptedDump, getFileExtension} from "./helpers";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
|
||||||
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();
|
||||||
|
const aesKeyHex = formData.get("aes_key") as string;
|
||||||
|
const ivHex = formData.get("iv") as string;
|
||||||
const generatedId = formData.get("generatedId") as string | null;
|
const generatedId = formData.get("generatedId") as string | null;
|
||||||
const method = formData.get("method") as string | null;
|
const method = formData.get("method") as string | null;
|
||||||
|
|
||||||
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,
|
||||||
|
alertPolicies: 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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,61 +106,90 @@ export async function POST(
|
|||||||
|
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
const file = formData.get("file") as File | null;
|
const file = formData.get("file") as File | null;
|
||||||
|
const extension = formData.get("extension") as string | null;
|
||||||
|
if (!aesKeyHex || !ivHex) {
|
||||||
|
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const fileSizeBytes = file.size;
|
||||||
|
// const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || '');
|
||||||
|
const fileExtension = extension ? extension : getFileExtension(database.dbms)
|
||||||
|
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const fileName = `${uuid}.dump`;
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
const { success, message, filePath } = await uploadLocalPrivate(fileName, buffer);
|
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",
|
fileSize: fileSizeBytes,
|
||||||
},
|
status: 'success',
|
||||||
});
|
}))
|
||||||
eventEmitter.emit('modification', { update: true });
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
await sendNotificationsBackupRestore(database, "success_backup");
|
||||||
|
|
||||||
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});
|
||||||
|
|
||||||
|
await sendNotificationsBackupRestore(database, "error_backup");
|
||||||
|
|
||||||
|
|
||||||
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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
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";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
|
||||||
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,14 +19,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)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -33,18 +33,17 @@ 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
|
with: {
|
||||||
|
alertPolicies: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,36 +51,31 @@ export async function POST(
|
|||||||
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({
|
|
||||||
where:{
|
await db
|
||||||
id : restoration.id
|
.update(drizzleDb.schemas.restoration)
|
||||||
},
|
.set({status: body.status as RestorationStatus})
|
||||||
data: {
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
status: body.status
|
|
||||||
}
|
|
||||||
})
|
await sendNotificationsBackupRestore(database, body.status == "error" ? "error_restore" : "success_restore");
|
||||||
|
|
||||||
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) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -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 {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,138 @@ 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({
|
const [databaseUpdated] = await dbClient
|
||||||
where: {
|
.update(drizzleDb.schemas.database)
|
||||||
databaseId: databaseUpdated.id,
|
.set(withUpdatedAt({
|
||||||
status: "waiting"
|
name: db.name,
|
||||||
}
|
agentId: agent.id,
|
||||||
})
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const restoration = await prisma.restoration.findFirst({
|
|
||||||
where:{
|
const backup = await dbClient.query.backup.findFirst({
|
||||||
databaseId: databaseUpdated.id,
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||||
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: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
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";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
dbms: Dbms,
|
dbms: EDbmsSchema,
|
||||||
generatedId: string
|
generatedId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
|
version: string,
|
||||||
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({fileName: "d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
|
||||||
return Response.json({
|
return Response.json({
|
||||||
message: url
|
message: url
|
||||||
})
|
})
|
||||||
@@ -32,26 +36,39 @@ 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(withUpdatedAt({
|
||||||
|
version: body.version,
|
||||||
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
@@ -61,6 +78,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);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { auth } from "@/lib/auth/auth";
|
||||||
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
|
export const { GET, POST } = toNextJsHandler(auth.handler);
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import {handlers} from "@/auth/auth";
|
|
||||||
|
|
||||||
|
|
||||||
export const {GET, POST} = handlers;
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
PROJECT_URL: process.env.PROJECT_URL,
|
||||||
|
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||||
|
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||||
|
});
|
||||||
|
}
|
||||||
+22
-12
@@ -1,9 +1,19 @@
|
|||||||
import { EventEmitter } from 'events';
|
import {EventEmitter} from 'events';
|
||||||
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
import {headers} from "next/headers";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
export const eventEmitter = new EventEmitter();
|
export const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
console.log('GET request received');
|
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
@@ -37,13 +47,13 @@ export async function GET(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
// export async function POST(request: Request) {
|
||||||
console.log('POST request received');
|
// console.log('POST request received');
|
||||||
const data = await request.json();
|
// const data = await request.json();
|
||||||
console.log('Data received:', data);
|
// console.log('Data received:', data);
|
||||||
|
//
|
||||||
// Emit the event to all connected clients
|
// // Emit the event to all connected clients
|
||||||
eventEmitter.emit('modification', data);
|
// eventEmitter.emit('modification', data);
|
||||||
|
//
|
||||||
return new Response('Event sent', { status: 200 });
|
// return new Response('Event sent', {status: 200});
|
||||||
}
|
// }
|
||||||
@@ -12,16 +12,16 @@ export async function GET(
|
|||||||
const expires = searchParams.get('expires');
|
const expires = searchParams.get('expires');
|
||||||
const fileName = (await params).fileName
|
const fileName = (await params).fileName
|
||||||
|
|
||||||
const privateLocalDir = "private/uploads/";
|
const uploadsDir = "private/uploads/files/";
|
||||||
const filePath = path.join(privateLocalDir, fileName);
|
const uploadPath = path.join(uploadsDir, fileName);
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
let filePath = null;
|
||||||
return NextResponse.json(
|
if (fs.existsSync(uploadPath)) {
|
||||||
{error: 'File not found'},
|
filePath = uploadPath;
|
||||||
{status: 404}
|
} else {
|
||||||
);
|
return NextResponse.json({error: "File not found"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
||||||
@@ -32,7 +32,7 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = parseInt(expires, 10);
|
const expiresAt = parseInt(expires!, 10);
|
||||||
if (Date.now() > expiresAt) {
|
if (Date.now() > expiresAt) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Signed token expired'},
|
{error: 'Signed token expired'},
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
import {headers} from "next/headers";
|
||||||
|
import {checkFileExistsInBucket, getObjectFromClient} from "@/utils/s3-file-management";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
import * as stream from "node:stream";
|
||||||
|
import path from "path";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
function nodeStreamToWebStream(nodeStream: stream.Readable) {
|
||||||
|
return new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
nodeStream.on("data", chunk => controller.enqueue(chunk));
|
||||||
|
nodeStream.on("end", () => controller.close());
|
||||||
|
nodeStream.on("error", err => controller.error(err));
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
nodeStream.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateS3ImageDir = "images/";
|
||||||
|
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request,
|
||||||
|
{params}: { params: Promise<{ fileName: string }> }
|
||||||
|
) {
|
||||||
|
const fileName = (await params).fileName;
|
||||||
|
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||||
|
|
||||||
|
const session = await auth.api.getSession({headers: await headers()});
|
||||||
|
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||||
|
|
||||||
|
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.");
|
||||||
|
|
||||||
|
const storageType = settings.storage; // "local" or "s3"
|
||||||
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
|
const contentType =
|
||||||
|
ext === "png"
|
||||||
|
? "image/png"
|
||||||
|
: ext === "jpg" || ext === "jpeg"
|
||||||
|
? "image/jpeg"
|
||||||
|
: ext === "gif"
|
||||||
|
? "image/gif"
|
||||||
|
: ext === "webp"
|
||||||
|
? "image/webp"
|
||||||
|
: "application/octet-stream";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (storageType === "local") {
|
||||||
|
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
const file = await fs.readFile(filePath);
|
||||||
|
|
||||||
|
return new NextResponse(file, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// if not found locally, fallback to S3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await checkFileExistsInBucket({
|
||||||
|
bucketName: env.S3_BUCKET_NAME!,
|
||||||
|
fileName: `${privateS3ImageDir}${fileName}`,
|
||||||
|
});
|
||||||
|
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
|
||||||
|
|
||||||
|
const nodeStream = await getObjectFromClient({
|
||||||
|
bucketName: env.S3_BUCKET_NAME!,
|
||||||
|
fileName: `${privateS3ImageDir}${fileName}`,
|
||||||
|
});
|
||||||
|
const webStream = nodeStreamToWebStream(nodeStream);
|
||||||
|
|
||||||
|
return new NextResponse(webStream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error streaming image:", err);
|
||||||
|
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
+2
-2
@@ -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
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
+127
-90
@@ -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
+1
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
+16
-9
@@ -1,27 +1,34 @@
|
|||||||
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 "./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";
|
||||||
|
import {inter} from "@/fonts/fonts";
|
||||||
|
|
||||||
|
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||||
const inter = Inter({subsets: ["latin"]});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Portabase",
|
title: {
|
||||||
description: "Manage all your database instances from one place !",
|
default: title,
|
||||||
|
template: `%s - ${title}`
|
||||||
|
},
|
||||||
|
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<meta name="apple-mobile-web-app-title" content={title}/>
|
||||||
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body className={cn(inter.className, "h-full")}>
|
||||||
|
<ConsoleSilencer/>
|
||||||
<Providers>
|
<Providers>
|
||||||
{children}
|
{children}
|
||||||
</Providers>
|
</Providers>
|
||||||
|
|||||||
Executable
+21
@@ -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"
|
||||||
|
}
|
||||||
+13
-12
@@ -1,14 +1,15 @@
|
|||||||
"use client"
|
import BackButton from "@/components/wrappers/common/button/back-button";
|
||||||
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
|
<BackButton>Go home</BackButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+11
-10
@@ -1,29 +1,30 @@
|
|||||||
"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";
|
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
||||||
|
|
||||||
|
|
||||||
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"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
enableSystem
|
enableSystem
|
||||||
// disableTransitionOnChange
|
|
||||||
>
|
>
|
||||||
|
<ThemeMetaUpdaterRoot/>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster/>
|
<Toaster/>
|
||||||
{props.children}
|
{props.children}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</Suspense>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
"iconLibrary": "radix",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@reui": "https://reui.io/r/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-23
@@ -3,23 +3,22 @@ name: portabase-prod
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
# target: prod
|
||||||
|
image: solucetechnologies/portabase:1.1.4-rc.2
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
container_name: portabase-app-prod
|
container_name: portabase-app-prod
|
||||||
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
@@ -36,20 +35,5 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
s3:
|
|
||||||
image: docker.io/bitnami/minio:latest
|
|
||||||
ports:
|
|
||||||
- '9000:9000'
|
|
||||||
- '9001:9001'
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=SoluceTechnologies
|
|
||||||
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
|
|
||||||
- MINIO_DEFAULT_BUCKETS=statics
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
|
|||||||
@@ -1,36 +1,6 @@
|
|||||||
name: portabase-dev
|
name: portabase-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
|
||||||
target: dev
|
|
||||||
ports:
|
|
||||||
- '8887:80'
|
|
||||||
environment:
|
|
||||||
- TIME_ZONE="Europe/Paris"
|
|
||||||
- NODE_ENV=development
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- /app/node_modules
|
|
||||||
container_name: portabase-app
|
|
||||||
|
|
||||||
develop:
|
|
||||||
watch:
|
|
||||||
- action: sync
|
|
||||||
path: .
|
|
||||||
target: /app
|
|
||||||
ignore:
|
|
||||||
- node_modules/
|
|
||||||
- action: rebuild
|
|
||||||
path: package.json
|
|
||||||
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
@@ -47,22 +17,5 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
s3:
|
|
||||||
image: docker.io/bitnami/minio:latest
|
|
||||||
ports:
|
|
||||||
- '9000:9000'
|
|
||||||
- '9001:9001'
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=SoluceTechnologies
|
|
||||||
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
|
|
||||||
- MINIO_DEFAULT_BUCKETS=statics
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
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
|
tzdata \
|
||||||
|
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 +30,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 +61,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; \
|
||||||
@@ -75,14 +83,17 @@ COPY --from=builder /app/public ./public
|
|||||||
|
|
||||||
RUN mkdir .next
|
RUN mkdir .next
|
||||||
RUN chown nextjs:nodejs .next
|
RUN chown nextjs:nodejs .next
|
||||||
|
RUN mkdir -p /app/private/uploads
|
||||||
|
RUN chown -R nextjs:nodejs /app/private
|
||||||
|
RUN chown -R nextjs:nodejs /app/public
|
||||||
|
|
||||||
COPY --from=builder /app/next.config.mjs ./
|
|
||||||
COPY --from=builder /app/schema.zmodel ./
|
COPY --from=builder /app/next.config.ts ./
|
||||||
#COPY --from=builder /app/src/lib/hooks ./src/lib/hooks
|
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
|
||||||
|
|
||||||
@@ -98,3 +109,5 @@ ENV HOSTNAME="0.0.0.0"
|
|||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
npm install -g npm@11.0.0
|
set -euo pipefail
|
||||||
npm -v
|
|
||||||
|
|
||||||
|
echo "▶ Running Drizzle codegen..."
|
||||||
|
npx drizzle-kit generate
|
||||||
|
|
||||||
current_date=$(date +"%Y-%m-%d")
|
echo "▶ Applying migrations..."
|
||||||
npx zenstack generate
|
npx drizzle-kit migrate
|
||||||
npx prisma migrate dev --name $current_date
|
|
||||||
|
|
||||||
npm run dev
|
echo "▶ Starting Next.js dev server..."
|
||||||
|
exec npm run dev
|
||||||
exec "$@"
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
npx install --save-dev @zenstackhq/openapi
|
if [ -n "$TZ" ]; then
|
||||||
|
echo "[INFO] Application timezone set to $TZ (environment only)"
|
||||||
|
export TZ="$TZ"
|
||||||
|
else
|
||||||
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
npx zenstack generate
|
|
||||||
npx prisma migrate deploy
|
|
||||||
node server.js
|
node server.js
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
||||||
|
|||||||
@@ -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!,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import {
|
|
||||||
Button as ReactEmailButton
|
|
||||||
} from "@react-email/components";
|
|
||||||
import {ComponentPropsWithoutRef} from "react";
|
|
||||||
|
|
||||||
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
|
|
||||||
return (
|
|
||||||
<ReactEmailButton
|
|
||||||
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Container,
|
|
||||||
Head,
|
|
||||||
Html,
|
|
||||||
Img,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Tailwind,
|
|
||||||
} from "@react-email/components";
|
|
||||||
import * as React from "react";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const baseUrl = getServerUrl();
|
|
||||||
|
|
||||||
export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: string }>) => {
|
|
||||||
return (
|
|
||||||
<Tailwind>
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
|
|
||||||
<Body className="bg-gray-100 py-4"
|
|
||||||
style={{fontFamily: "Arial, sans-serif"}}>
|
|
||||||
|
|
||||||
<Container className="bg-white border border-gray-200 p-12">
|
|
||||||
<Img
|
|
||||||
src={`${baseUrl}/logo-title-black.png`}
|
|
||||||
width="200"
|
|
||||||
height="auto"
|
|
||||||
alt="Logo"
|
|
||||||
/>
|
|
||||||
<Section>
|
|
||||||
{children}
|
|
||||||
</Section>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
</Tailwind>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default EmailLayout;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Button } from "@react-email/button";
|
|
||||||
import { Html } from "@react-email/html";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function HelloEmail() {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Button
|
|
||||||
href="https://example.com"
|
|
||||||
style={{ background: "#000", color: "#fff" }}
|
|
||||||
>
|
|
||||||
Hello Charles
|
|
||||||
</Button>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import {
|
|
||||||
Text,
|
|
||||||
} from "@react-email/components";
|
|
||||||
import * as React from "react";
|
|
||||||
import EmailLayout from "./EmailLayout";
|
|
||||||
import {Button} from "@react-email/button";
|
|
||||||
|
|
||||||
|
|
||||||
export const TestEmailSettings = () => {
|
|
||||||
return (
|
|
||||||
<EmailLayout
|
|
||||||
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>
|
|
||||||
|
|
||||||
</EmailLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default TestEmailSettings;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import {
|
|
||||||
Text as ReactEmailText
|
|
||||||
} from "@react-email/components";
|
|
||||||
import {ComponentPropsWithoutRef} from "react";
|
|
||||||
|
|
||||||
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
|
|
||||||
return (
|
|
||||||
<ReactEmailText
|
|
||||||
className="text-base font-light leading-8 text-green-800 "
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {defineConfig, globalIgnores} from 'eslint/config'
|
||||||
|
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
globalIgnores([
|
||||||
|
'.next/**',
|
||||||
|
'out/**',
|
||||||
|
'build/**',
|
||||||
|
'next-env.d.ts',
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default eslintConfig
|
||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
import {init} from "@/utils/init";
|
|
||||||
|
|
||||||
|
|
||||||
export async function register() {
|
export async function register() {
|
||||||
init()
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
const init = await import("@/utils/init");
|
||||||
|
await init.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import {NextRequest, NextResponse} from 'next/server'
|
|
||||||
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
|
||||||
import {errorHandler} from "@/middleware/errorHandler";
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
// If the route does not exist, return a 404 JSON response
|
|
||||||
if (!routeExists) {
|
|
||||||
return new NextResponse(
|
|
||||||
JSON.stringify({ message: "This API route does not exist.", status: 404 }),
|
|
||||||
{ status: 404, headers: { 'Content-Type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try{
|
|
||||||
loggingMiddleware(request);
|
|
||||||
}catch(err){
|
|
||||||
errorHandler(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
// Function to check if the route exists (supports dynamic routes)
|
|
||||||
function checkRouteExists(pathname) {
|
|
||||||
// Define static and dynamic routes with patterns
|
|
||||||
const routePatterns = [
|
|
||||||
//do not delete
|
|
||||||
// /^\/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\/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\/[^/]+\/backup\/?$/,
|
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
|
||||||
/^\/api\/files\/[^/]+\/?$/,
|
|
||||||
/^\/api\/events\/?$/,
|
|
||||||
];
|
|
||||||
return routePatterns.some(pattern => pattern.test(pathname));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
// '/api/agent/:path*',
|
|
||||||
'/api/:path*',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Vendored
-20
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/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.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user