mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
157
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+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="* * * * *"
|
||||||
@@ -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,28 @@
|
|||||||
|
name: Security Checks
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sca-deps: # Dependency & container scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: aquasecurity/trivy-action@0.20.0
|
||||||
|
with:
|
||||||
|
scan-type: 'fs'
|
||||||
|
format: 'table'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
|
secrets-gitleaks: # Secrets exposure
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history for gitleaks
|
||||||
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
|
with:
|
||||||
|
config-path: .gitleaks.toml # Optional, if you have a custom config
|
||||||
+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
|
|
||||||
|
|||||||
@@ -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,308 @@
|
|||||||
<!-- 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">
|
Free, open-source, and self-hosted solution for automated backup and restoration of your database instances.
|
||||||
Backup / Restore your databases instances
|
|
||||||
<br />
|
|
||||||
<a href="https://portabase.io"><strong>Explore the docs »</strong></a>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<a href="https://portabase.io">View Demo</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||||
|
[](https://github.com/RostislavDugin/postgresus)
|
||||||
|
|
||||||
|
[](https://www.postgresql.org/)
|
||||||
|
[](https://www.mysql.com/)
|
||||||
|
[](https://mariadb.org/)
|
||||||
|
[](https://github.com/RostislavDugin/postgresus)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<a href="https://portabase.io">Documentation</a> •
|
||||||
|
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">Demo</a> •
|
||||||
|
<a href="#installation">Installation</a> •
|
||||||
|
<a href="#contributing">Contributing</a> •
|
||||||
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📚 Table of Contents
|
||||||
|
|
||||||
|
- [About The Project](#-about-the-project)
|
||||||
|
- [Getting Started](#-getting-started)
|
||||||
|
- [Usage](#-usage)
|
||||||
|
- [Roadmap](#-roadmap)
|
||||||
|
- [Contributing](#-contributing)
|
||||||
|
- [License](#-license)
|
||||||
|
- [Contact](#-contact)
|
||||||
|
- [Acknowledgments](#-acknowledgments)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 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 for managing operations securely and efficiently.
|
||||||
|
|
||||||
|
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
|
||||||
|
|
||||||
|
### 🔧 Built With
|
||||||
|
|
||||||
|
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
|
||||||
|
- [![Drizzle][Drizzle]][Drizzle-url]
|
||||||
|
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||||
|
- [![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
|
- [![Docker][Docker]][Docker-url]
|
||||||
|
|
||||||
|
|
||||||
<!-- TABLE OF CONTENTS -->
|
---
|
||||||
<details>
|
|
||||||
<summary>Table of Contents</summary>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
<a href="#about-the-project">About The Project</a>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#built-with">Built With</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#getting-started">Getting Started</a>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
|
||||||
<li><a href="#installation">Installation</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><a href="#usage">Usage</a></li>
|
|
||||||
<li><a href="#roadmap">Roadmap</a></li>
|
|
||||||
<li><a href="#contributing">Contributing</a></li>
|
|
||||||
<li><a href="#license">License</a></li>
|
|
||||||
<li><a href="#contact">Contact</a></li>
|
|
||||||
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
<!-- ABOUT THE PROJECT -->
|
|
||||||
## 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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Built With
|
|
||||||
|
|
||||||
* [![NextJS][NextJS]][NextJS-url]
|
|
||||||
* [![Prisma][Prisma]][Prisma-url]
|
|
||||||
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- GETTING STARTED -->
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Install docker on your machine
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Installation steps in order to use it locally and work on some features improvements
|
Ensure Docker is installed on your machine before getting started.
|
||||||
|
|
||||||
1. Clone the repo
|
### Option 1: Docker Compose Setup
|
||||||
```sh
|
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
|
||||||
```
|
|
||||||
3. Start the project
|
|
||||||
```sh
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
Create a `docker-compose.yml` file with the following configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: portabase
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
services:
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [ ] Add Changelog
|
portabase:
|
||||||
- [ ] Improve documentation and repositories
|
image: solucetechnologies/portabase:latest
|
||||||
- [ ] Add tests procedure
|
env_file:
|
||||||
- [ ] Add Release File
|
- .env
|
||||||
- [ ] Security improvements
|
ports:
|
||||||
- [ ] Multi locale support
|
- '8887:80'
|
||||||
- [ ] Multi db support
|
environment:
|
||||||
|
- TIME_ZONE="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/portabase/advanced-topics/reverse-proxy)
|
||||||
|
|
||||||
|
### Option 2: Locally (Development)
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Soluce-Technologies/portabase
|
||||||
|
cd portabase
|
||||||
|
```
|
||||||
|
2. Start the development environment:
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Usage
|
||||||
|
|
||||||
|
Portabase provides a web dashboard to manage your database instances and backups.
|
||||||
|
It currently supports:
|
||||||
|
|
||||||
|
- **PostgreSQL**
|
||||||
|
- **MySQL**
|
||||||
|
|
||||||
|
### 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/Soluce-Technologies/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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
- [ ] Add changelog
|
||||||
|
- [ ] Enhance documentation
|
||||||
|
- [ ] Implement testing procedures
|
||||||
|
- [ ] Include release file
|
||||||
|
- [ ] Improve security
|
||||||
|
- [ ] Enhance UX/UI
|
||||||
|
- [ ] Improve workspace management system
|
||||||
|
- [x] Migrate to Drizzle ORM
|
||||||
|
- [ ] Add multi-language support
|
||||||
|
- [ ] Extend multi-database support:
|
||||||
- [x] PostgreSQL
|
- [x] PostgreSQL
|
||||||
- [ ] MongoDB
|
- [ ] MongoDB
|
||||||
- [ ] MySQL
|
- [x] MySQL
|
||||||
|
- [x] MariaDB
|
||||||
|
|
||||||
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues).
|
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome and appreciated! Here's how to get started:
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
1. Fork the repository
|
||||||
## Contributing
|
2. Create a new branch:
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/YourFeature
|
||||||
|
```
|
||||||
|
3. Commit your changes:
|
||||||
|
```bash
|
||||||
|
git commit -m "Add YourFeature"
|
||||||
|
```
|
||||||
|
4. Push to the branch:
|
||||||
|
```bash
|
||||||
|
git push origin feature/YourFeature
|
||||||
|
```
|
||||||
|
5. Open a pull request
|
||||||
|
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
Give the project a ⭐ if you like it!
|
||||||
|
|
||||||
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
### Top Contributors
|
||||||
Don't forget to give the project a star! Thanks again!
|
|
||||||
|
|
||||||
1. Fork the Project
|
[](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
|
||||||
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
||||||
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
||||||
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
### Top contributors:
|
---
|
||||||
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors">
|
## Developer Notes
|
||||||
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
### Environment Variables
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
## Memo
|
# Database
|
||||||
|
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||||
|
|
||||||
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format:
|
# Project Info
|
||||||
|
PROJECT_NAME="Portabase"
|
||||||
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
|
PROJECT_URL=http://app.portabase.io
|
||||||
|
PROJECT_SECRET=
|
||||||
|
|
||||||
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)```
|
# SMTP (Email)
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_FROM=
|
||||||
|
|
||||||
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)```
|
# Google OAuth
|
||||||
|
AUTH_GOOGLE_ID=
|
||||||
|
AUTH_GOOGLE_SECRET=
|
||||||
|
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
|
||||||
|
|
||||||
<!-- LICENSE -->
|
# Storage Backend: 'local' or 's3'
|
||||||
## License
|
STORAGE_TYPE=local
|
||||||
|
|
||||||
Distributed under the Apache License. See `LICENSE.txt` for more information.
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
|
```
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
To get more information about env variables, check
|
||||||
|
that [link](https://portabase.io/docs/portabase/advanced-topics/environment)
|
||||||
|
|
||||||
|
### Semantic Versioning
|
||||||
|
|
||||||
<!-- CONTACT -->
|
Use the following format for Docker image versioning:
|
||||||
## Contact
|
|
||||||
|
|
||||||
Killian Larcher - killian.larcher@soluce-technologies.com
|
```bash
|
||||||
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
major.minor.patch-rc.release
|
||||||
|
# Example: 1.0.0-rc.1
|
||||||
|
|
||||||
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase)
|
major.minor.patch-rc.release-tag
|
||||||
|
# Example: 1.0.0-rc.1-dev
|
||||||
|
```
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
---
|
||||||
|
|
||||||
<!-- MARKDOWN LINKS & IMAGES -->
|
## 📄 License
|
||||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
||||||
|
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📬 Contact
|
||||||
|
|
||||||
|
- Killian Larcher - killian.larcher@soluce-technologies.com
|
||||||
|
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
||||||
|
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
Thanks to all contributors and the open-source community!
|
||||||
|
|
||||||
|
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
|
||||||
|
|
||||||
[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,15 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {ForgotPasswordForm} from "@/components/wrappers/auth/forgot-password/forgot-password-form";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Forgot Password",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid w-full gap-6">
|
||||||
|
<ForgotPasswordForm/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+10
-22
@@ -1,29 +1,17 @@
|
|||||||
"use client"
|
import {env} from "@/env.mjs";
|
||||||
|
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
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";
|
const authGoogleEnabled = env.AUTH_GOOGLE_METHOD;
|
||||||
|
|
||||||
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">
|
<div className="mx-auto grid w-full gap-6">
|
||||||
<LoginForm/>
|
<LoginForm authGoogleEnabled={authGoogleEnabled}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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,15 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {ResetPasswordSection} from "@/components/wrappers/auth/reset-password/reset-password-section";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Reset Password",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid w-full gap-6">
|
||||||
|
<ResetPasswordSection/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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,32 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||||
|
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/columns-users";
|
||||||
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
accounts: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active users</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<DataTable
|
||||||
|
enableSelect={false}
|
||||||
|
columns={usersColumnsAdmin}
|
||||||
|
data={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,95 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
import {formatDateLastContact} from "@/utils/date-formatting";
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
|
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {Server} from "lucide-react";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
|
||||||
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">
|
{agent.description && (
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||||
|
)}
|
||||||
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:justify-between gap-6 ">
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
Backups
|
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
||||||
</CardHeader>
|
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||||
<CardContent></CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Success rate
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{successRate ?? "Unavailable for now."}
|
<div className="text-2xl font-bold">{agent.databases.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Databases linked to this agent</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
Last contact
|
<CardTitle className="text-sm font-medium">Last contact</CardTitle>
|
||||||
|
<Server className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{formatDateLastContact(agent.lastContact)}
|
<div className="text-2xl font-bold">{formatDateLastContact(agent.lastContact)}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Last contact with agent</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<Card className="w-full sm:w-auto flex-1 ">
|
||||||
<CardHeader className="font-bold text-xl">
|
<CardHeader className="font-bold text-xl">
|
||||||
Edge Key
|
Edge Key
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<AgentCardKey agent={agent}/>
|
<AgentCardKey
|
||||||
|
edgeKey={edgeKey}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/>
|
<CardsWithPagination cardsPerPage={4} numberOfColumns={2} 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,35 @@
|
|||||||
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 {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))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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 +38,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
// import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{
|
||||||
|
databaseId: string;
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const {databaseId} = await props.params;
|
||||||
|
|
||||||
|
const user = await currentUser();
|
||||||
|
if (!user) notFound();
|
||||||
|
|
||||||
|
const [dbToRestore] = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.id, databaseId));
|
||||||
|
|
||||||
|
if (!dbToRestore) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbsOfSameType = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.dbms!, dbToRestore.dbms!));
|
||||||
|
const successfulBackups = await db.select().from(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.status, "success"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Restore {dbToRestore.name}</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
{/*<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups}/>*/}
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
+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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {notFound, redirect} from "next/navigation";
|
||||||
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
|
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
|
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
||||||
|
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||||
|
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
|
||||||
|
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||||
|
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {eq, and, inArray} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||||
|
import {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";
|
||||||
|
|
||||||
|
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=" w-full md: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} />
|
||||||
|
</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,55 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
const isOwner = activeMember?.role === "owner";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle className="flex items-center">
|
||||||
|
Organization settings
|
||||||
|
{!isMember && organization.slug !== "default" && (
|
||||||
|
<EditButtonSettings/>
|
||||||
|
)}
|
||||||
|
</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
{isOwner && organization.slug !== "default" && (
|
||||||
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
|
)}
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<OrganizationTabs
|
||||||
|
organization={organization}
|
||||||
|
notificationChannels={notificationChannels}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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()
|
||||||
|
);
|
||||||
|
|
||||||
|
const Placeholder = ({text}: { text: string }) => (
|
||||||
|
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Evolution of the number of backups</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{sortedBackupsEvolution.length > 0 ? (
|
||||||
|
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
||||||
|
) : (
|
||||||
|
<Placeholder text="No backup data available"/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Success rate of backups</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{backupsRate.length > 0 ? (
|
||||||
|
<PercentageLineChart data={backupsRate}/>
|
||||||
|
) : (
|
||||||
|
<Placeholder text="No backup rate data available"/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
|
||||||
databaseId: string;
|
|
||||||
}>) {
|
|
||||||
|
|
||||||
const {databaseId} = await props.params
|
|
||||||
|
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
|
|
||||||
const database = await prisma.database.findUnique({
|
|
||||||
where: {
|
|
||||||
id: databaseId,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const databases = await prisma.database.findMany({
|
|
||||||
where: {
|
|
||||||
dbms: database.dbms,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const backups = await prisma.backup.findMany({
|
|
||||||
where: {
|
|
||||||
status: "success",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (!database) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Restore {database.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {redirect} from "next/navigation";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="flex flex-1 flex-col gap-4 p-4">
|
|
||||||
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
|
||||||
// <div className="aspect-video rounded-xl bg-muted/50"/>
|
|
||||||
// <div className="aspect-video rounded-xl bg-muted/50"/>
|
|
||||||
// <div className="aspect-video rounded-xl bg-muted/50"/>
|
|
||||||
// </div>
|
|
||||||
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
|
||||||
// </div>
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
-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,77 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
|
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
|
||||||
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
|
|
||||||
|
|
||||||
const {slug: organizationSlug, projectId} = await props.params
|
|
||||||
|
|
||||||
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
|
||||||
|
|
||||||
if(currentOrganizationSlug != organizationSlug) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const project = await prisma.project.findUnique({
|
|
||||||
where: {
|
|
||||||
id: projectId,
|
|
||||||
isArchived: {not: true}
|
|
||||||
},
|
|
||||||
include:{
|
|
||||||
databases: {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if(!project){
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="justify-between gap-2 sm:flex">
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
{project.name}
|
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
|
||||||
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
|
|
||||||
<GearIcon className="w-7 h-7"/>
|
|
||||||
</Link>
|
|
||||||
</PageTitle>
|
|
||||||
<PageActions className="justify-between">
|
|
||||||
<ButtonDeleteProject
|
|
||||||
projectId={projectId}
|
|
||||||
text={"Delete Project"}
|
|
||||||
/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
|
||||||
<PageDescription>
|
|
||||||
The list of associated databases
|
|
||||||
</PageDescription>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
|
||||||
{project.databases.length > 0 ?
|
|
||||||
<CardsWithPagination
|
|
||||||
data={project.databases}
|
|
||||||
organizationSlug={organizationSlug}
|
|
||||||
cardItem={ProjectDatabaseCard}
|
|
||||||
cardsPerPage={4}
|
|
||||||
numberOfColumns={1}
|
|
||||||
extendedProps={project}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|
||||||
const {slug: organizationSlug} = await props.params
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
|
||||||
if(currentOrganizationSlug != organizationSlug) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const availableDatabases = await prisma.database.findMany({
|
|
||||||
where: {
|
|
||||||
projectId: null
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
agent: {}
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const organization = await prisma.organization.findFirst({
|
|
||||||
where: {
|
|
||||||
slug: currentOrganizationSlug,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Create new project
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<ProjectForm databases={availableDatabases} organization={organization}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
|
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
|
||||||
import {currentOrganization} from "@/auth/current-organization";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|
||||||
const {slug: organizationSlug} = await props.params
|
|
||||||
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
|
||||||
|
|
||||||
if(currentOrganizationSlug != organizationSlug) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const projects = await prisma.project.findMany({
|
|
||||||
where: {
|
|
||||||
isArchived: {not: true},
|
|
||||||
|
|
||||||
organization: {
|
|
||||||
slug: organizationSlug,
|
|
||||||
// users: {
|
|
||||||
// some: {
|
|
||||||
// userId: user.id
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
databases: {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Projects
|
|
||||||
</PageTitle>
|
|
||||||
{projects.length > 0 && (
|
|
||||||
<PageActions>
|
|
||||||
<Link href={`/dashboard/${currentOrganizationSlug}/projects/new`}>
|
|
||||||
<Button>+ Create Project</Button>
|
|
||||||
</Link>
|
|
||||||
</PageActions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<PageContent className="mt-10">
|
|
||||||
|
|
||||||
{projects.length > 0 ?
|
|
||||||
<CardsWithPagination
|
|
||||||
organizationSlug={currentOrganizationSlug}
|
|
||||||
data={projects}
|
|
||||||
cardItem={ProjectCard}
|
|
||||||
cardsPerPage={4}
|
|
||||||
numberOfColumns={1}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/${currentOrganizationSlug}/projects/new`}
|
|
||||||
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
|
|
||||||
Create new Project
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
}
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
|
||||||
slug: string;
|
|
||||||
}>) {
|
|
||||||
const {slug: organizationSlug} = await props.params
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
if(currentOrganizationSlug != organizationSlug) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await prisma.organization.findUnique({
|
|
||||||
where:{
|
|
||||||
slug: currentOrganizationSlug,
|
|
||||||
},
|
|
||||||
include:{
|
|
||||||
users:{
|
|
||||||
include:{
|
|
||||||
user:{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
user: {
|
|
||||||
id: {
|
|
||||||
not: user.id, // Exclude user with id: 1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
|
||||||
where: {
|
|
||||||
deleted: {not: true},
|
|
||||||
id: {not: user.id}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return(
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Edit {organization.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<OrganizationForm users={users} defaultValues={organization}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import {prisma} from "@/prisma";
|
|
||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
|
||||||
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
|
|
||||||
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
|
||||||
import {
|
|
||||||
DeleteOrganizationButton
|
|
||||||
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
|
|
||||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
|
|
||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
|
||||||
|
|
||||||
const organization = await prisma.organization.findUnique({
|
|
||||||
where: {
|
|
||||||
slug: currentOrganizationSlug,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
users:{
|
|
||||||
include:{
|
|
||||||
user: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentOrganizationUser = await prisma.userOrganization.findFirst({
|
|
||||||
where:{
|
|
||||||
userId: user.id,
|
|
||||||
organization:{
|
|
||||||
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (currentOrganizationUser.role != "admin") {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const settings = await prisma.settings.findUnique({
|
|
||||||
where: {
|
|
||||||
name: "system"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
Settings
|
|
||||||
{organization.slug != "default" ?
|
|
||||||
<EditButtonSettings/>
|
|
||||||
: null}
|
|
||||||
</PageTitle>
|
|
||||||
<PageActions>
|
|
||||||
{organization.slug != "default" ?
|
|
||||||
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
|
|
||||||
: null}
|
|
||||||
</PageActions>
|
|
||||||
</PageHeader>
|
|
||||||
<PageDescription>
|
|
||||||
Manage your organization settings.
|
|
||||||
</PageDescription>
|
|
||||||
<PageContent>
|
|
||||||
<SettingsTabs settings={settings} currentUser={user} users={organization.users}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
|
||||||
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
|
||||||
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const projectsCount = await prisma.project.count({
|
|
||||||
where: {
|
|
||||||
organization: {
|
|
||||||
slug: "default"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupsEvolution = await prisma.backup.findMany({
|
|
||||||
select: {
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "asc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const backupsRate = await prisma.backup.groupBy({
|
|
||||||
by: ['createdAt', 'status'],
|
|
||||||
where: {
|
|
||||||
status: {
|
|
||||||
in: ['success', 'failed'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Statistics
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Projects</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{projectsCount}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>KPI 2</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>KPI 3</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<EvolutionLineChart data={backupsEvolution}/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Success rate of backups</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<PercentageLineChart data={backupsRate}/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 from "react";
|
||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"
|
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
||||||
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
|
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
||||||
import {currentUser} from "@/auth/current-user";
|
|
||||||
import {Header} from "@/features/layout/Header";
|
import {Header} from "@/features/layout/Header";
|
||||||
import {prisma} from "@/prisma";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {GlobalStoreProvider} from "@/state-management/provider";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default async function Layout({children}: { children: React.ReactNode }) {
|
||||||
|
const user = await currentUser();
|
||||||
const user = await currentUser()
|
if (!user) redirect("/login");
|
||||||
if (user) {
|
|
||||||
const userInfo = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email: user?.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!userInfo) redirect('/login')
|
|
||||||
}
|
|
||||||
if (!user) redirect('/login')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <HydrationZustand>
|
|
||||||
// <GlobalStoreProvider>
|
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<AppSidebar/>
|
<AppSidebar/>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header/>
|
<Header/>
|
||||||
<main className="h-full">
|
<main className="h-full">{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
</>
|
||||||
// </GlobalStoreProvider>
|
);
|
||||||
// </HydrationZustand>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -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 +1,62 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
|
|
||||||
import {prisma} from "@/prisma";
|
|
||||||
import {Badge} from "@/components/ui/badge";
|
import {Badge} from "@/components/ui/badge";
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {getAccounts, getSessions} from "@/lib/auth/auth";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Profile",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
const user = await currentUser();
|
||||||
const user = await requiredCurrentUser()
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
notFound()
|
return notFound();
|
||||||
}
|
}
|
||||||
const userInfo = await prisma.user.findUnique({
|
|
||||||
where: {
|
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
||||||
email: user.email
|
return notFound();
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
const sessions = await getSessions();
|
||||||
|
const accounts = await getAccounts();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
<AvatarWithUpload user={userInfo}/>
|
<AvatarWithUpload
|
||||||
{user.name}
|
user={{
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
|
...user,
|
||||||
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
|
image: user.image ?? null,
|
||||||
</PageTitle>
|
role: user.role ?? null,
|
||||||
<PageActions className="mt-2 hidden sm:block">
|
banned: user.banned ?? null,
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
banReason: user.banReason ?? null,
|
||||||
</PageActions>
|
banExpires: user.banExpires ?? null,
|
||||||
</div>
|
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{user.name}
|
||||||
|
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
||||||
|
</PageTitle>
|
||||||
|
</div>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<UserForm userId={userInfo.id} defaultValues={userInfo}/>
|
<UserForm
|
||||||
<div className="mt-4 sm:hidden">
|
userId={user.id}
|
||||||
<ButtonDeleteAccount text="Delete my account" />
|
sessions={sessions}
|
||||||
</div>
|
accounts={accounts}
|
||||||
|
defaultValues={{
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role ?? undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,60 +107,87 @@ 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;
|
||||||
|
|
||||||
if (!file) {
|
if (!aesKeyHex || !ivHex) {
|
||||||
return NextResponse.json(
|
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
||||||
{ error: "File is required for successful backup" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = uuidv4();
|
|
||||||
const fileName = `${uuid}.dump`;
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
|
||||||
|
|
||||||
const { success, message, filePath } = await uploadLocalPrivate(fileName, buffer);
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "File is required for successful backup"},
|
||||||
|
{status: 400}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const fileExtension = getFileExtension(database.dbms)
|
||||||
|
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
||||||
|
const uuid = uuidv4();
|
||||||
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let success: boolean, message: string, filePath: string;
|
||||||
|
|
||||||
|
const result =
|
||||||
|
settings.storage === "local"
|
||||||
|
? await uploadLocalPrivate(fileName, buffer)
|
||||||
|
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||||
|
|
||||||
|
({success, message, filePath} = result);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: message },
|
{error: message},
|
||||||
{ status: 500 }
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.backup.update({
|
await db
|
||||||
where: { id: backup.id },
|
.update(drizzleDb.schemas.backup)
|
||||||
data: {
|
.set(withUpdatedAt({
|
||||||
file: fileName,
|
file: fileName,
|
||||||
status: "success",
|
status: 'success',
|
||||||
},
|
}))
|
||||||
});
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
eventEmitter.emit('modification', { update: true });
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
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 {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {SafeActionResult} from "next-safe-action";
|
||||||
|
import {ZodString} from "zod";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
|
|
||||||
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
|
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
|
||||||
generatedId: database.generatedId,
|
generatedId: database.agentDatabaseId,
|
||||||
dbms: database.dbms,
|
dbms: database.dbms,
|
||||||
data: {
|
data: {
|
||||||
backup: {
|
backup: {
|
||||||
@@ -25,97 +32,137 @@ 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({
|
||||||
status: "waiting"
|
agentId: agent.id,
|
||||||
}
|
lastContact: lastContact
|
||||||
})
|
|
||||||
|
|
||||||
const restoration = await prisma.restoration.findFirst({
|
|
||||||
where:{
|
|
||||||
databaseId: databaseUpdated.id,
|
|
||||||
status: "waiting"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if(backup){
|
|
||||||
backupAction = true
|
|
||||||
await prisma.backup.update({
|
|
||||||
where:{
|
|
||||||
id: backup.id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: "ongoing"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
const backup = await dbClient.query.backup.findFirst({
|
||||||
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
await dbClient
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
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,14 +1,16 @@
|
|||||||
import {prisma} from "@/prisma";
|
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Dbms} from "@prisma/client";
|
|
||||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
||||||
import {handleDatabases} from "./helpers";
|
import {handleDatabases} from "./helpers";
|
||||||
import {eventEmitter} from "../../../events/route";
|
import {eventEmitter} from "../../../events/route";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
dbms: Dbms,
|
dbms: EDbmsSchema,
|
||||||
generatedId: string
|
generatedId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,9 +18,9 @@ export type Body = {
|
|||||||
databases: databaseAgent[]
|
databases: databaseAgent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to test the get file url presigned local
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
const url = await getFileUrlPresignedLocal({fileName:"d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
|
||||||
return Response.json({
|
return Response.json({
|
||||||
message: url
|
message: url
|
||||||
})
|
})
|
||||||
@@ -32,26 +34,36 @@ export async function POST(
|
|||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
|
let message: string
|
||||||
|
|
||||||
const agent = await prisma.agent.findFirst({
|
|
||||||
where: {
|
if (!isUuidv4(agentId)) {
|
||||||
id: agentId
|
message = "agentId is not a valid uuid"
|
||||||
}
|
console.log(message)
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "agentId is not a valid uuid"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
message = "Agent not found"
|
||||||
|
console.log(message)
|
||||||
|
return NextResponse.json({error: message}, {status: 404})
|
||||||
}
|
}
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
||||||
await prisma.agent.update({
|
|
||||||
where:{
|
|
||||||
id: agentId,
|
|
||||||
},
|
|
||||||
data:{
|
|
||||||
lastContact: lastContact,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', { update: true });
|
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.agent)
|
||||||
|
.set({lastContact: lastContact})
|
||||||
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
@@ -61,6 +73,7 @@ export async function POST(
|
|||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
console.log(response)
|
console.log(response)
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
|
|||||||
@@ -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: 11 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
+3
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 90 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
+13
-9
@@ -1,17 +1,19 @@
|
|||||||
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 function RootLayout({
|
||||||
@@ -21,10 +23,12 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
||||||
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body className={cn(inter.className, "h-full")}>
|
||||||
<Providers>
|
<ConsoleSilencer/>
|
||||||
{children}
|
<Providers>{children}</Providers>
|
||||||
</Providers>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+12
-18
@@ -1,29 +1,23 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {PropsWithChildren} from "react";
|
import { PropsWithChildren, Suspense } from "react";
|
||||||
import {ThemeProvider} from "@/features/theme/theme-provider";
|
import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||||
|
|
||||||
import {Toaster} from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import {SessionProvider} from "next-auth/react";
|
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<Suspense fallback={null}>
|
||||||
<ThemeProvider
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
attribute="class"
|
|
||||||
defaultTheme="system"
|
|
||||||
enableSystem
|
|
||||||
// disableTransitionOnChange
|
|
||||||
>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster/>
|
<Toaster />
|
||||||
{props.children}
|
{props.children}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</Suspense>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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; \
|
||||||
@@ -74,17 +82,18 @@ RUN adduser --system --uid 1001 nextjs
|
|||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
RUN mkdir .next
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
RUN mkdir -p /app/private/uploads
|
RUN mkdir -p /app/private/uploads
|
||||||
RUN chown -R nextjs:nodejs /app/private
|
RUN chown -R nextjs:nodejs /app/private
|
||||||
|
RUN chown -R nextjs:nodejs /app/public
|
||||||
|
|
||||||
RUN chown nextjs:nodejs .next
|
|
||||||
|
|
||||||
COPY --from=builder /app/next.config.mjs ./
|
COPY --from=builder /app/next.config.ts ./
|
||||||
COPY --from=builder /app/schema.zmodel ./
|
COPY --from=builder /app/portabase.config.ts ./
|
||||||
COPY --from=builder /app/src/openapi.yaml ./src/
|
COPY --from=builder /app/drizzle.config.ts ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
COPY --chown=nextjs:nodejs prisma ./prisma/
|
COPY --chown=nextjs:nodejs src/db ./src/db
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
@@ -100,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,15 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo " ____ __ __ "
|
if [ -n "$TZ" ]; then
|
||||||
echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
|
echo "[INFO] Application timezone set to $TZ (environment only)"
|
||||||
echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
export TZ="$TZ"
|
||||||
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
else
|
||||||
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
echo " "
|
fi
|
||||||
echo " Community Edition "
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
output: "standalone",
|
|
||||||
typescript: {
|
|
||||||
//TODO fix all type errors and remove 'ignoreBuildErrors'
|
|
||||||
// !! WARN !!
|
|
||||||
// Dangerously allow production builds to successfully complete even if
|
|
||||||
// your project has type errors.
|
|
||||||
// !! WARN !!
|
|
||||||
ignoreBuildErrors: true,
|
|
||||||
},
|
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
// bundlePagesRouterDependencies: true,
|
|
||||||
compiler: {
|
|
||||||
removeConsole: {
|
|
||||||
exclude: ['error'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type {NextConfig} from "next";
|
||||||
|
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
||||||
|
|
||||||
|
|
||||||
|
function buildCSPHeader(): string {
|
||||||
|
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
|
|
||||||
|
const directives = [
|
||||||
|
`default-src ${CSP.DEFAULT_SRC.join(" ")}`,
|
||||||
|
`script-src ${CSP.SCRIPT_SRC.join(" ")}`,
|
||||||
|
`style-src ${CSP.STYLE_SRC.join(" ")}`,
|
||||||
|
`img-src ${CSP.IMG_SRC.join(" ")}`,
|
||||||
|
`font-src ${CSP.FONT_SRC.join(" ")}`,
|
||||||
|
`object-src ${CSP.OBJECT_SRC.join(" ")}`,
|
||||||
|
`connect-src ${CSP.CONNECT_SRC.join(" ")}`,
|
||||||
|
`base-uri ${CSP.BASE_URI.join(" ")}`,
|
||||||
|
`form-action ${CSP.FORM_ACTION.join(" ")}`,
|
||||||
|
`frame-ancestors ${CSP.FRAME_ANCESTORS.join(" ")}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (CSP.BLOCK_ALL_MIXED_CONTENT) {
|
||||||
|
directives.push("block-all-mixed-content");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CSP.UPGRADE_INSECURE_REQUESTS) {
|
||||||
|
directives.push("upgrade-insecure-requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
return directives.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPermissionsPolicy(): string {
|
||||||
|
return Object.entries(PORTABASE_DEFAULT_SETTINGS.SECURITY.PERMISSIONS_POLICY)
|
||||||
|
.map(([feature, values]) => `${feature.toLowerCase()}=${values.join(", ")}`)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/(.*)",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Content-Security-Policy",
|
||||||
|
value: buildCSPHeader(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Permissions-Policy",
|
||||||
|
value: buildPermissionsPolicy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Content-Type-Options',
|
||||||
|
value: 'nosniff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'DENY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Referrer-Policy',
|
||||||
|
value: 'strict-origin-when-cross-origin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Strict-Transport-Security',
|
||||||
|
value: 'max-age=63072000; includeSubDomains; preload',
|
||||||
|
}
|
||||||
|
// ...other security headers
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
+104
-88
@@ -1,105 +1,121 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "0.1.0",
|
"version": "1.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 80",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
"build": "next build",
|
"build": "next build --experimental-build-mode compile",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"email": "email dev"
|
"email": "email dev --dir ./src/components/emails",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:drop": "drizzle-kit drop",
|
||||||
|
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.7.4",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@radix-ui/react-accordion": "^1.2.10",
|
||||||
"@prisma/client": "^6.1.0",
|
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-accordion": "^1.2.1",
|
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.9",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-checkbox": "^1.3.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-collapsible": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.1.2",
|
"@radix-ui/react-context-menu": "^2.2.14",
|
||||||
"@radix-ui/react-collapsible": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-context-menu": "^2.2.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-hover-card": "^1.1.2",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-icons": "^1.3.1",
|
"@radix-ui/react-menubar": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||||
"@radix-ui/react-menubar": "^1.1.2",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
"@radix-ui/react-progress": "^1.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@radix-ui/react-radio-group": "^1.3.6",
|
||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||||
"@radix-ui/react-radio-group": "^1.2.1",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-separator": "^1.1.6",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-slider": "^1.3.4",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-slider": "^1.2.1",
|
"@radix-ui/react-switch": "^1.2.4",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.11",
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-toast": "^1.2.13",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-toggle": "^1.1.8",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@react-email/components": "^0.0.41",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@t3-oss/env-nextjs": "^0.13.4",
|
||||||
"@react-email/components": "^0.0.28",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tanstack/react-query": "^5.62.11",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/ws": "^8.5.13",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"@zenstackhq/runtime": "2.10.2",
|
"argon2": "^0.43.0",
|
||||||
"argon2": "^0.41.1",
|
"bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^5.1.1",
|
"better-auth": "1.4.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.6",
|
||||||
"embla-carousel-react": "^8.3.1",
|
"dotenv": "^16.5.0",
|
||||||
"input-otp": "^1.4.0",
|
"drizzle-orm": "^0.43.1",
|
||||||
"lucide-react": "^0.468.0",
|
"drizzle-zod": "^0.7.1",
|
||||||
"minio": "^8.0.2",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"next": "15.1.3",
|
"input-otp": "^1.4.2",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"lucide-react": "^0.553.0",
|
||||||
"next-safe-action": "^7.10.2",
|
"minio": "^8.0.5",
|
||||||
"next-themes": "^0.3.0",
|
"motion": "^12.23.24",
|
||||||
"nodemailer": "^6.9.16",
|
"next": "16.0.7",
|
||||||
"npm-check-updates": "^17.1.13",
|
"next-safe-action": "^7.10.8",
|
||||||
"react": "19.0.0",
|
"next-themes": "^0.4.6",
|
||||||
"react-day-picker": "8.10.1",
|
"node-cron": "^4.2.1",
|
||||||
"react-dom": "19.0.0",
|
"node-forge": "^1.3.1",
|
||||||
"react-email": "^3.0.2",
|
"nodemailer": "^7.0.3",
|
||||||
"react-hook-form": "^7.53.1",
|
"npm-check-updates": "^18.0.1",
|
||||||
"react-resizable-panels": "^2.1.6",
|
"pg": "^8.16.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-day-picker": "9.7.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
|
"react-email": "^4.0.13",
|
||||||
|
"react-hook-form": "^7.56.3",
|
||||||
|
"react-resizable-panels": "^3.0.2",
|
||||||
"react-twc": "^1.4.2",
|
"react-twc": "^1.4.2",
|
||||||
"recharts": "^2.15.0",
|
"react-use-measure": "^2.1.7",
|
||||||
|
"recharts": "^2.15.3",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"sonner": "^1.6.1",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"uuid": "^11.1.0",
|
||||||
"uuid": "^11.0.3",
|
"vaul": "^1.1.2",
|
||||||
"vaul": "^1.1.1",
|
"ws": "^8.18.2",
|
||||||
"ws": "^8.18.0",
|
"zod": "^3.24.4"
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint-plugin-tailwindcss": "^3",
|
"@iconify/react": "^6.0.0",
|
||||||
"@types/node": "^20",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@zenstackhq/openapi": "^2.10.2",
|
"@types/node": "^22.15.18",
|
||||||
"@zenstackhq/tanstack-query": "^2.10.2",
|
"@types/node-forge": "^1",
|
||||||
"eslint": "^8",
|
"@types/pg": "^8.15.2",
|
||||||
"eslint-config-next": "15.0.3",
|
"@types/react": "^19.1.4",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"postcss": "^8",
|
"@zenstackhq/openapi": "^2.14.2",
|
||||||
"prisma": "^6.1.0",
|
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
"typescript": "^5",
|
"drizzle-kit": "^0.31.1",
|
||||||
"zenstack": "2.10.2"
|
"eslint": "^9.39.0",
|
||||||
|
"eslint-config-next": "^16.0.1",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^4.1.7",
|
||||||
|
"tsx": "^4.19.4",
|
||||||
|
"tw-animate-css": "^1.2.9",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"packageManager": "yarn@4.9.1"
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
export const PORTABASE_DEFAULT_SETTINGS = {
|
||||||
|
SECURITY: {
|
||||||
|
CSP: {
|
||||||
|
DEFAULT_SRC: ["'self'"],
|
||||||
|
SCRIPT_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-eval'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"https://cdn.jsdelivr.net",
|
||||||
|
"https://www.googletagmanager.com",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
|
||||||
|
],
|
||||||
|
STYLE_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'",
|
||||||
|
"https://fonts.googleapis.com",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
],
|
||||||
|
IMG_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"blob:",
|
||||||
|
"data:",
|
||||||
|
"https:",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
"https://code.iconify.com",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
"http://localhost:9000",
|
||||||
|
],
|
||||||
|
FONT_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"https://fonts.gstatic.com",
|
||||||
|
"https://cdn.iconify.design",
|
||||||
|
],
|
||||||
|
CONNECT_SRC: [
|
||||||
|
"'self'",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
"https://code.iconify.design",
|
||||||
|
"https://api.github.com",
|
||||||
|
|
||||||
|
],
|
||||||
|
OBJECT_SRC: ["'none'"],
|
||||||
|
BASE_URI: ["'self'"],
|
||||||
|
FORM_ACTION: ["'self'"],
|
||||||
|
FRAME_ANCESTORS: ["'none'"],
|
||||||
|
BLOCK_ALL_MIXED_CONTENT: false,
|
||||||
|
UPGRADE_INSECURE_REQUESTS: true,
|
||||||
|
},
|
||||||
|
PERMISSIONS_POLICY: {
|
||||||
|
CAMERA: ["()"],
|
||||||
|
MICROPHONE: ["()"],
|
||||||
|
GEOLOCATION: ["()"],
|
||||||
|
FULLSCREEN: ["(self)"],
|
||||||
|
// ...other features
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user