Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4447180e9e | ||
|
|
f111946592 | ||
|
|
475e404fae | ||
|
|
07ce5215d4 | ||
|
|
9f4895fd63 | ||
|
|
c18a9da0d3 | ||
|
|
8e3eb32631 | ||
|
|
f0598b3f59 | ||
|
|
011120fdf9 | ||
|
|
ff175e1edd | ||
|
|
3f56008c39 | ||
|
|
5df95bf7c1 | ||
|
|
23d52d0447 | ||
|
|
795a127998 | ||
|
|
fd73d383f9 | ||
|
|
ee285df7b5 | ||
|
|
7f7f8e642f | ||
|
|
d9997ff5e9 | ||
|
|
36adcb323a | ||
|
|
5302060f44 | ||
|
|
d19a46bee6 | ||
|
|
d504866aad | ||
|
|
601dae4909 | ||
|
|
08f2ad0521 | ||
|
|
3b356e33e7 | ||
|
|
57d9b8da1e | ||
|
|
86f50e5741 | ||
|
|
a6e52fd03d | ||
|
|
116229a29e | ||
|
|
428782e8a4 | ||
|
|
b8442803d9 | ||
|
|
82ab2c63d6 | ||
|
|
e953de0e06 | ||
|
|
f6d927f866 | ||
|
|
3773c4b068 | ||
|
|
bb0dbdadae | ||
|
|
dd8aec50eb | ||
|
|
7410aaea38 | ||
|
|
0f9bbbda3c | ||
|
|
25f9bc6e79 | ||
|
|
caf00b7877 | ||
|
|
9c0c462192 | ||
|
|
ef7570db76 | ||
|
|
e1bc1ec6c8 | ||
|
|
ca232250cc | ||
|
|
fde7688485 | ||
|
|
9458b12d69 | ||
|
|
5cf6cf075e | ||
|
|
8980e6d4a8 | ||
|
|
ac951378e8 | ||
|
|
934e635cdb | ||
|
|
d968aa9c16 | ||
|
|
eb471c7cdb | ||
|
|
e886bdd58d | ||
|
|
fe5d8bae1b | ||
|
|
aabdea06a9 | ||
|
|
37a3ace161 | ||
|
|
933ab7d68a | ||
|
|
f42cb37d67 | ||
|
|
7dfd9f02e8 | ||
|
|
411e09f452 | ||
|
|
f21149f42e | ||
|
|
099b6fb0e8 | ||
|
|
eb2e0ef468 | ||
|
|
4644ed2f41 | ||
|
|
cf8e5b7094 | ||
|
|
b0775e82ea | ||
|
|
f9b3461a44 | ||
|
|
c4ebc876bf | ||
|
|
a435a92d41 | ||
|
|
bcbd6bbea1 | ||
|
|
82559b49bd | ||
|
|
8c1b869004 | ||
|
|
c4a0692985 | ||
|
|
750f1cb145 | ||
|
|
b7d3487c53 | ||
|
|
529f157c6e | ||
|
|
c62be9854a | ||
|
|
9c1aa66b41 | ||
|
|
9894e2582d | ||
|
|
8a8ef81fce | ||
|
|
2fdd9f1296 | ||
|
|
5251e01011 | ||
|
|
84dab35f4b | ||
|
|
ae766dab2f | ||
|
|
027928c29f | ||
|
|
e3239639f3 | ||
|
|
7d6038e5f1 | ||
|
|
2a90d4933d | ||
|
|
1fbd979d40 | ||
|
|
4e06d2bf97 | ||
|
|
8ce3988613 | ||
|
|
b7f161d200 | ||
|
|
a3f5c4e494 | ||
|
|
f74f378c0b | ||
|
|
385836d5fe | ||
|
|
461e3ff7c9 | ||
|
|
54a233b207 | ||
|
|
2d2453acf7 | ||
|
|
654ff6b9a5 | ||
|
|
34ff11f377 | ||
|
|
b95e145a8b | ||
|
|
bce6c9d55f | ||
|
|
c3e2978919 | ||
|
|
2d559e456d | ||
|
|
45e3b31fda | ||
|
|
c8da2579e8 | ||
|
|
c7e30e1659 | ||
|
|
7be23876aa | ||
|
|
98f1b329ae | ||
|
|
19ea5543ea | ||
|
|
010396ba4d | ||
|
|
f055744432 | ||
|
|
1920b29a55 | ||
|
|
a0c8d1fb68 | ||
|
|
59dc3b71b8 | ||
|
|
598eb44952 | ||
|
|
ac49047177 | ||
|
|
4448c9e166 | ||
|
|
d70778b556 | ||
|
|
66edb77113 | ||
|
|
0f7b3eae11 | ||
|
|
2f9e33f4db | ||
|
|
dd7a65262c | ||
|
|
4c0d2b1f7f | ||
|
|
60728885b6 | ||
|
|
7ac99a6618 | ||
|
|
4ed91c1297 | ||
|
|
44be8009fd | ||
|
|
96630a0ef7 | ||
|
|
8c9d694b40 | ||
|
|
1f09c1507a | ||
|
|
141241d555 | ||
|
|
c605e8173b | ||
|
|
37eb8f319f | ||
|
|
0492d244b3 | ||
|
|
baeec9f7a0 | ||
|
|
449c9c2cfe | ||
|
|
d13e4b7d40 | ||
|
|
13241811d0 | ||
|
|
4b52dc4a1e | ||
|
|
63cb6cb7a3 | ||
|
|
0511ad289c | ||
|
|
057a51499e | ||
|
|
1f01d9c372 | ||
|
|
0684a4887a | ||
|
|
96be14e1ba | ||
|
|
eec3c2737b | ||
|
|
a6b960ffb2 | ||
|
|
d3f465b5d4 | ||
|
|
2139dfd55d | ||
|
|
9f53181467 | ||
|
|
c96e13c72f | ||
|
|
58d9179152 | ||
|
|
17469696ee | ||
|
|
85047e7509 | ||
|
|
f9dc15ba73 | ||
|
|
7be542f676 | ||
|
|
847a567c52 | ||
|
|
1ffbd6efc8 | ||
|
|
f8f599e4dd | ||
|
|
f7cd169090 | ||
|
|
bb4003b0db | ||
|
|
159398465f | ||
|
|
6562718635 |
@@ -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
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ NODE_ENV=production
|
|||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||||
|
|
||||||
# Projet
|
# Projet
|
||||||
NEXT_PUBLIC_PROJECT_NAME="Portabase"
|
PROJECT_NAME="Portabase"
|
||||||
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
|
PROJECT_URL=http://app.portabase.io
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
# SMTP (email)
|
# SMTP (email)
|
||||||
@@ -20,6 +20,7 @@ SMTP_FROM=
|
|||||||
# Google
|
# Google
|
||||||
AUTH_GOOGLE_ID=
|
AUTH_GOOGLE_ID=
|
||||||
AUTH_GOOGLE_SECRET=
|
AUTH_GOOGLE_SECRET=
|
||||||
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
# S3
|
# S3
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
S3_ENDPOINT=http://app.s3.portabase.io
|
||||||
@@ -31,3 +32,6 @@ S3_USE_SSL=true
|
|||||||
|
|
||||||
# Storage Type (s3, local)
|
# Storage Type (s3, local)
|
||||||
STORAGE_TYPE=local
|
STORAGE_TYPE=local
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
@@ -48,22 +48,3 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.set-tags.outputs.tags }}
|
tags: ${{ steps.set-tags.outputs.tags }}
|
||||||
target: prod
|
target: prod
|
||||||
|
|
||||||
# Do not delete
|
|
||||||
# - name: Build and push Docker image
|
|
||||||
# id: push
|
|
||||||
# uses: docker/build-push-action@v6
|
|
||||||
# with:
|
|
||||||
# context: .
|
|
||||||
# file: ./docker/dockerfile/Dockerfile
|
|
||||||
# push: true
|
|
||||||
# tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
# labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
# target: prod
|
|
||||||
|
|
||||||
# - name: Generate artifact attestation
|
|
||||||
# uses: actions/attest-build-provenance@v2
|
|
||||||
# with:
|
|
||||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
|
||||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
# push-to-registry: true
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Security Checks
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sca-deps: # Dependency & container scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: aquasecurity/trivy-action@0.20.0
|
||||||
|
with:
|
||||||
|
scan-type: 'fs'
|
||||||
|
format: 'table'
|
||||||
|
severity: 'CRITICAL,HIGH'
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
|
secrets-gitleaks: # Secrets exposure
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history for gitleaks
|
||||||
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
|
with:
|
||||||
|
config-path: .gitleaks.toml # Optional, if you have a custom config
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -26,7 +26,7 @@ Please take a moment to review this guide. It will help you understand how to co
|
|||||||
|
|
||||||
2. **Clone the repository**
|
2. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
git clone https://github.com/Portabase/portabase
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up the development environment**
|
3. **Set up the development environment**
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
migrate:
|
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
||||||
docker compose run --rm app
|
|
||||||
|
up:
|
||||||
|
@bash $(CLUSTER_SCRIPT)
|
||||||
@@ -1,70 +1,134 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://portabase.io">
|
<a href="https://portabase.io">
|
||||||
<img src="images/logo.png" alt="Logo" width="80" height="80">
|
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
|
||||||
</a>
|
</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.
|
||||||
Easily backup and restore your database instances with Portabase
|
|
||||||
<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=hvLbX5LN1UE">Demo</a> •
|
||||||
|
<a href="#installation">Installation</a> •
|
||||||
|
<a href="#contributing">Contributing</a> •
|
||||||
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📚 Table of Contents
|
## 📚 Table of Contents
|
||||||
|
|
||||||
- [About The Project](#about-the-project)
|
- [About The Project](#-about-the-project)
|
||||||
- [Built With](#built-with)
|
- [Getting Started](#-getting-started)
|
||||||
- [Getting Started](#getting-started)
|
- [Usage](#-usage)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Roadmap](#-roadmap)
|
||||||
- [Installation](#installation)
|
- [Contributing](#-contributing)
|
||||||
- [Usage](#usage)
|
- [License](#-license)
|
||||||
- [Roadmap](#roadmap)
|
- [Contact](#-contact)
|
||||||
- [Contributing](#contributing)
|
- [Acknowledgments](#-acknowledgments)
|
||||||
- [License](#license)
|
|
||||||
- [Contact](#contact)
|
|
||||||
- [Acknowledgments](#acknowledgments)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## About The Project
|
## ✨ 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.
|
**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)
|
GitHub Repository: [Portabase](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
### 🔧 Built With
|
### 🔧 Built With
|
||||||
|
|
||||||
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
|
- [![NextJS][NextJS]][NextJS-url] (v16 with App Router)
|
||||||
- [![Drizzle][Drizzle]][Drizzle-url]
|
- [![Drizzle][Drizzle]][Drizzle-url]
|
||||||
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||||
- BetterAuth
|
- [![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
- React Email
|
- [![Docker][Docker]][Docker-url]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Installation
|
||||||
|
|
||||||
Ensure Docker is installed on your machine before getting started.
|
Ensure Docker is installed on your machine before getting started.
|
||||||
|
|
||||||
### Installation
|
### Option 1: Docker Compose Setup
|
||||||
|
|
||||||
To run Portabase locally:
|
Create a `docker-compose.yml` file with the following configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: portabase
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
portabase:
|
||||||
|
image: solucetechnologies/portabase:latest
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- '8887:80'
|
||||||
|
environment:
|
||||||
|
- TZ="Europe/Paris"
|
||||||
|
volumes:
|
||||||
|
- portabase-private:/app/private
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
container_name: portabase-app
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=<your_database>
|
||||||
|
- POSTGRES_USER=<database_user>
|
||||||
|
- POSTGRES_PASSWORD=<database_password>
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U <database_user> -d <your_database>" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
portabase-private:
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use reverse proxy like
|
||||||
|
Traefik : [Check this link](https://portabase.io/docs/dashboard/advanced/reverse-proxy)
|
||||||
|
|
||||||
|
### Option 2: Locally (Development)
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
git clone https://github.com/Portabase/portabase
|
||||||
cd portabase
|
cd portabase
|
||||||
```
|
```
|
||||||
2. Start the development environment:
|
2. Start the development environment:
|
||||||
@@ -76,13 +140,23 @@ To run Portabase locally:
|
|||||||
|
|
||||||
## 🛠️ Usage
|
## 🛠️ Usage
|
||||||
|
|
||||||
Portabase provides a dashboard to manage database instances and backups. It supports:
|
Portabase provides a web dashboard to manage your database instances and backups.
|
||||||
|
It currently supports:
|
||||||
|
|
||||||
- PostgreSQL (current)
|
- **PostgreSQL**
|
||||||
- MongoDB (coming soon)
|
- **MySQL**
|
||||||
- MySQL (coming soon)
|
|
||||||
|
|
||||||
You can access the dashboard at `http://localhost:3000` after starting the project.
|
### Process
|
||||||
|
|
||||||
|
1. **Access the dashboard** – Open `http://localhost:8887` in your browser.
|
||||||
|
2. **Sign up** – Register the first user, who will automatically have the **Admin** role in the default workspace.
|
||||||
|
3. **Add your first agent** – Follow [this guide](https://github.com/Portabase/agent-portabase) for setup
|
||||||
|
instructions.
|
||||||
|
4. **Create organizations and projects** – Link your databases to projects to enable backups and restores.
|
||||||
|
5. **Configure backup policies** – Define schedules (hourly, daily, weekly, or monthly) and retention rules.
|
||||||
|
6. **Choose a storage provider** – Select where backups will be stored (local, S3, etc.).
|
||||||
|
7. **Save and start** – Portabase validates your configuration and starts automated backups based on your defined
|
||||||
|
policies.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -100,9 +174,10 @@ You can access the dashboard at `http://localhost:3000` after starting the proje
|
|||||||
- [ ] Extend multi-database support:
|
- [ ] Extend multi-database support:
|
||||||
- [x] PostgreSQL
|
- [x] PostgreSQL
|
||||||
- [ ] MongoDB
|
- [ ] MongoDB
|
||||||
- [ ] MySQL
|
- [x] MySQL
|
||||||
|
- [x] MariaDB
|
||||||
|
|
||||||
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
|
Check out [open issues](https://github.com/Portabase/portabase/issues) for more.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -129,7 +204,7 @@ Give the project a ⭐ if you like it!
|
|||||||
|
|
||||||
### Top Contributors
|
### Top Contributors
|
||||||
|
|
||||||
[](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -141,27 +216,28 @@ Give the project a ⭐ if you like it!
|
|||||||
# Environment
|
# Environment
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
||||||
|
|
||||||
# Project Info
|
# Project Info
|
||||||
NEXT_PUBLIC_PROJECT_NAME="Portabase"
|
PROJECT_NAME="Portabase"
|
||||||
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
|
PROJECT_URL=http://app.portabase.io
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
# SMTP (Email)
|
# SMTP (Email)
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM=
|
SMTP_FROM=
|
||||||
|
|
||||||
# Google OAuth
|
# Google OAuth
|
||||||
AUTH_GOOGLE_ID=
|
AUTH_GOOGLE_ID=
|
||||||
AUTH_GOOGLE_SECRET=
|
AUTH_GOOGLE_SECRET=
|
||||||
|
AUTH_GOOGLE_METHOD=
|
||||||
|
|
||||||
# S3/MinIO Configuration
|
# S3/MinIO Configuration
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
S3_ENDPOINT=http://app.s3.portabase.io
|
||||||
S3_ACCESS_KEY=
|
S3_ACCESS_KEY=
|
||||||
S3_SECRET_KEY=
|
S3_SECRET_KEY=
|
||||||
@@ -169,10 +245,16 @@ S3_BUCKET_NAME=portabase
|
|||||||
S3_PORT=9000
|
S3_PORT=9000
|
||||||
S3_USE_SSL=true
|
S3_USE_SSL=true
|
||||||
|
|
||||||
# Storage Backend: 'local' or 's3'
|
# Storage Backend: 'local' or 's3'
|
||||||
STORAGE_TYPE=local
|
STORAGE_TYPE=local
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To get more information about env variables, check
|
||||||
|
that [link](https://portabase.io/docs/portabase/advanced-topics/environment)
|
||||||
|
|
||||||
### Semantic Versioning
|
### Semantic Versioning
|
||||||
|
|
||||||
Use the following format for Docker image versioning:
|
Use the following format for Docker image versioning:
|
||||||
@@ -185,14 +267,6 @@ major.minor.patch-rc.release-tag
|
|||||||
# Example: 1.0.0-rc.1-dev
|
# Example: 1.0.0-rc.1-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zenstack Support
|
|
||||||
|
|
||||||
To regenerate schema:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
zenstack generate
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
@@ -205,7 +279,7 @@ Distributed under the Apache License. See `LICENSE.txt` for more details.
|
|||||||
|
|
||||||
- Killian Larcher - killian.larcher@soluce-technologies.com
|
- Killian Larcher - killian.larcher@soluce-technologies.com
|
||||||
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
||||||
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
|
- Project Link: [Portabase GitHub](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -213,11 +287,23 @@ Distributed under the Apache License. See `LICENSE.txt` for more details.
|
|||||||
|
|
||||||
Thanks to all contributors and the open-source community!
|
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
|
||||||
|
|
||||||
|
[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
|
[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
|
[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/
|
[Drizzle-url]: https://orm.drizzle.team/
|
||||||
|
|
||||||
[ShadcnUI-url]: https://ui.shadcn.com/
|
[ShadcnUI-url]: https://ui.shadcn.com/
|
||||||
|
|
||||||
|
[Docker-url]: https://www.docker.com/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {ForgotPasswordForm} from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Reset password</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a
|
||||||
|
link to reset your password.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ForgotPasswordForm/>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {GuardForm} from "@/components/wrappers/auth/guard/guard-form";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {redirect} from "next/navigation";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
|
export default async function GuardPage() {
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("better-auth.two_factor")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Two-factor verification</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Please enter the verification code generated
|
||||||
|
by your authentication app.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<GuardForm/>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {LayoutAdmin} from "@/components/layout";
|
import {redirect} from "next/navigation";
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (user && !user.banned && user.role !== "pending") {
|
||||||
|
redirect("/dashboard/home");
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,52 @@
|
|||||||
"use client"
|
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
|
||||||
|
import {SocialAuthButtons} from "@/components/wrappers/auth/social-buttons";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
import {useEffect, useRef} from "react";
|
export const metadata: Metadata = {
|
||||||
import {useSearchParams} from "next/navigation";
|
title: "Login",
|
||||||
|
};
|
||||||
|
|
||||||
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm";
|
export default async function SignInPage() {
|
||||||
import {toast} from "sonner";
|
|
||||||
|
|
||||||
export default function SignInPage(props: {
|
|
||||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const error = searchParams.get("error");
|
|
||||||
const isFirstRender = useRef(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstRender.current && error) {
|
|
||||||
toast.error("Error occurred.");
|
|
||||||
isFirstRender.current = false;
|
|
||||||
}
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-[350px] gap-6">
|
<TooltipProvider>
|
||||||
<LoginForm/>
|
<CardAuth className="w-full">
|
||||||
</div>
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Login</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Fill your login informations</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<LoginForm/>
|
||||||
|
|
||||||
|
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||||
|
<Separator/>
|
||||||
|
<div className="px-2 text-center text-sm">OR</div>
|
||||||
|
<Separator/>
|
||||||
|
</div>
|
||||||
|
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm">
|
||||||
|
Don't have an account ?{" "}
|
||||||
|
<Link href="/register" className="underline">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
|
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Register",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-[350px] gap-6">
|
<div className="mx-auto grid w-full gap-6">
|
||||||
<RegisterForm/>
|
<RegisterForm/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { auth } from "@/lib/auth/auth";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||||
|
|
||||||
|
const { token } = await props.searchParams;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${token}`);
|
||||||
|
|
||||||
|
if (!verification || verification.expiresAt < new Date()) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Card className="w-full max-w-md shadow-lg">
|
||||||
|
<CardHeader className="space-y-4">
|
||||||
|
<div className="space-y-1 text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
||||||
|
<p className="text-sm text-muted-foreground text-balance">Please enter your new password below.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center space-y-2 text-center">
|
||||||
|
<Avatar className="relative flex h-16 w-16 shrink-0 overflow-hidden rounded-full border">
|
||||||
|
<AvatarImage src={user!.image ?? ""} alt={user!.name} className="aspect-square h-full w-full object-cover" />
|
||||||
|
<AvatarFallback className="flex h-full w-full items-center justify-center rounded-full bg-muted text-2xl font-medium text-muted-foreground">
|
||||||
|
{user!.name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="font-semibold text-lg">{user!.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{user!.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page} from "@/features/layout/page";
|
||||||
|
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
|
||||||
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
|
import {isUUID} from "@/utils/text";
|
||||||
|
import {user} from "@/db/schema/02_user";
|
||||||
|
import {invitation} from "@/db/schema/05_invitation";
|
||||||
|
import {member} from "@/db/schema/04_member";
|
||||||
|
import {organization} from "@/db/schema/03_organization";
|
||||||
|
import {user as drizzleUser} from "@/db/schema/02_user";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{ organizationId: string }>) {
|
||||||
|
const {organizationId} = await props.params;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUUID(organizationId)) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await db.select().from(drizzleUser);
|
||||||
|
|
||||||
|
const organizationData = await db
|
||||||
|
.select({organization, member, user, invitation})
|
||||||
|
.from(organization)
|
||||||
|
.leftJoin(member, eq(drizzleDb.schemas.organization.id, member.organizationId))
|
||||||
|
.leftJoin(invitation, eq(drizzleDb.schemas.invitation.id, invitation.organizationId))
|
||||||
|
.leftJoin(user, eq(drizzleDb.schemas.member.userId, user.id))
|
||||||
|
.where(eq(organization.id, organizationId));
|
||||||
|
|
||||||
|
const formattedData = buildOrganizationWithMembers(organizationData);
|
||||||
|
|
||||||
|
if (!formattedData) return notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<OrganizationManagement organization={formattedData} users={users}/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {
|
||||||
|
AdminOrganizationAddModal
|
||||||
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||||
|
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active organizations</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminOrganizationAddModal/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminOrganizationList organizations={organizations}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { PageParams } from "@/types/next";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
|
||||||
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs";
|
|
||||||
import { currentUser } from "@/lib/auth/current-user";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import {eq, isNotNull, isNull} from "drizzle-orm";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
const user = await currentUser();
|
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
|
||||||
where: (fields) => isNull(fields.deletedAt)
|
|
||||||
});
|
|
||||||
// const users = await db.query.user.findMany();
|
|
||||||
console.log("users",users);
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, { eq }) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Administration Panel</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="mt-10">
|
|
||||||
<AdminTabs settings={settings!} users={users} />
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">System email</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsEmailSection settings={settings}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">System storage</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsStorageSection settings={settings}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
||||||
|
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
accounts: true
|
||||||
|
},
|
||||||
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
|
||||||
|
});
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active users</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminUserAddModal organizations={organizations}/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminUserList users={users}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
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 {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
import {db} from "@/db";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
|
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {generateEdgeKey} from "@/utils/edge_key";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||||
|
|
||||||
@@ -24,93 +23,36 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
databases: true
|
databases: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//
|
|
||||||
console.log(agent)
|
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// const databaseId = 'db-123';
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
//
|
|
||||||
// const totalBackupsResult = await db
|
|
||||||
// .select({ count: drizzleDb.schemas.backup.id })
|
|
||||||
// .from(drizzleDb.schemas.backup)
|
|
||||||
// .where(eq(drizzleDb.schemas.backup.databaseId, databaseId))
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
// const totalBackups = totalBackupsResult.length;
|
|
||||||
//
|
|
||||||
// const successfulBackupsResult = await db
|
|
||||||
// .select({ count: drizzleDb.schemas.backup.id })
|
|
||||||
// .from(drizzleDb.schemas.backup)
|
|
||||||
// .where(
|
|
||||||
// and(
|
|
||||||
// eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
|
||||||
// eq(drizzleDb.schemas.backup.status, "success")
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// const successfulBackups = successfulBackupsResult.length;
|
|
||||||
//
|
|
||||||
// const successRate =
|
|
||||||
// totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{agent.name}
|
{capitalizeFirstLetter(agent.name)}
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
href={`/dashboard/agents/${agent.id}/edit`}>
|
||||||
<GearIcon className="w-7 h-7"/>
|
<GearIcon className="w-7 h-7"/>
|
||||||
</Link>
|
</Link>
|
||||||
{/*<AgentModalKey agent={agent}>*/}
|
|
||||||
{/* <Button variant="outline">*/}
|
|
||||||
{/* <KeyRound/>*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</AgentModalKey>*/}
|
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
<PageActions className="justify-between">
|
||||||
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
||||||
|
</PageActions>
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Backups
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent></CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Success rate
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{/*{successRate ?? "Unavailable for now."}*/}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Last contact
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{formatDateLastContact(agent.lastContact)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</div>
|
{agent.description && (
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
||||||
<CardHeader className="font-bold text-xl">
|
)}
|
||||||
Edge Key
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
</CardHeader>
|
<AgentContentPage
|
||||||
<CardContent>
|
agent={agent}
|
||||||
<AgentCardKey agent={agent}/>
|
edgeKey={edgeKey}
|
||||||
</CardContent>
|
/>
|
||||||
</Card>
|
|
||||||
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
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 {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Agent",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
return (
|
return (
|
||||||
@@ -9,7 +14,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageTitle>Create new agent</PageTitle>
|
<PageTitle>Create new agent</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<AgentForm />
|
<AgentForm/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
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 { notFound } from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
export const dynamic = "force-dynamic";
|
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 db.query.agent.findMany();
|
|
||||||
|
const agents = await db.query.agent.findMany({
|
||||||
|
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
||||||
|
with: {
|
||||||
|
databases: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(agents);
|
||||||
|
|
||||||
|
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -27,16 +43,14 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent>
|
||||||
{agents.length > 0 ? (
|
{agents.length > 0 ? (
|
||||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<EmptyStatePlaceholder
|
||||||
href={"/dashboard/agents/new"}
|
url={"/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"
|
text={"Create new Agent"}
|
||||||
>
|
/>
|
||||||
Create new Agent
|
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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/RestoreForm";
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { authClient } from "@/lib/auth/auth-client";
|
|
||||||
import { PageParams } from "@/types/next";
|
|
||||||
|
|
||||||
export default function RoutePage(props: PageParams<{}>) {
|
|
||||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
|
||||||
|
|
||||||
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" />
|
|
||||||
{JSON.stringify(activeOrganization)}
|
|
||||||
{activeOrganization?.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
|
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
|
||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
@@ -27,6 +27,7 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
<PageContent>
|
<PageContent>
|
||||||
<DatabaseForm
|
<DatabaseForm
|
||||||
databaseId={databaseId}
|
databaseId={databaseId}
|
||||||
|
// @ts-ignore
|
||||||
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -1,25 +1,50 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound, redirect} from "next/navigation";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
||||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
|
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
||||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
|
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
||||||
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
|
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
||||||
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
|
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq, and} from "drizzle-orm";
|
import {eq, and, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||||
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
|
||||||
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
|
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
export default async function RoutePage(props: PageParams<{
|
||||||
const {databaseId} = await props.params;
|
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({
|
const dbItem = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
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) {
|
if (!dbItem) {
|
||||||
notFound();
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
const backups = await db.query.backup.findMany({
|
const backups = await db.query.backup.findMany({
|
||||||
@@ -38,37 +63,73 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
|
|||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const [totalBackups, successfulBackups] = await Promise.all([
|
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
db
|
.from(drizzleDb.schemas.backup)
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
||||||
.from(drizzleDb.schemas.backup)
|
.then(rows => rows.length);
|
||||||
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
|
|
||||||
.then((rows) => rows.length),
|
const availableBackups = backups.filter(b => !b.deletedAt).length;
|
||||||
db
|
|
||||||
.select({count: drizzleDb.schemas.backup.id})
|
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(and(eq(drizzleDb.schemas.backup.databaseId, dbItem.id), eq(drizzleDb.schemas.backup.status, "success")))
|
.where(and(
|
||||||
.then((rows) => rows.length),
|
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 successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
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 flex-col md:flex-row items-center justify-between w-full ">
|
||||||
{dbItem.name}
|
<div className="min-w-full md:min-w-fit ">
|
||||||
<EditButton/>
|
{capitalizeFirstLetter(dbItem.name)}
|
||||||
<CronButton database={dbItem}/>
|
</div>
|
||||||
|
{!isMember && (
|
||||||
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Do not delete*/}
|
||||||
|
{/*<EditButton/>*/}
|
||||||
|
<RetentionPolicySheet database={dbItem}/>
|
||||||
|
<CronButton database={dbItem}/>
|
||||||
|
<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}
|
||||||
|
organizationId={organization.id}/>
|
||||||
|
<ImportModal database={dbItem}/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
|
||||||
|
{dbItem.description && (
|
||||||
|
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
||||||
|
)}
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
|
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
||||||
<DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
|
totalBackups={totalBackups}/>
|
||||||
|
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem}
|
||||||
|
isAlreadyRestore={isAlreadyRestore}
|
||||||
|
backups={backups}
|
||||||
|
restorations={restorations}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
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 {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {DatabaseWith} from "@/db/schema/06_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
||||||
const {projectId} = await props.params;
|
const {projectId} = await props.params;
|
||||||
@@ -42,10 +42,9 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
// .filter((db): db is DatabaseWith => db.project !== null);
|
// .filter((db) => db.project !== null) as DatabaseWith[];
|
||||||
|
|
||||||
console.log("ici34",availableDatabases);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +56,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
<ProjectForm
|
<ProjectForm
|
||||||
organization={org}
|
organization={org}
|
||||||
databases={availableDatabases as DatabaseWith[]}
|
databases={availableDatabases as DatabaseWith[]}
|
||||||
defaultValues={{name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id)}}
|
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
|
||||||
projectId={proj.id}
|
projectId={proj.id}
|
||||||
/>
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import {buttonVariants} from "@/components/ui/button";
|
||||||
import { GearIcon } from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
|
import {
|
||||||
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
ButtonDeleteProject
|
||||||
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||||
import { notFound } from "next/navigation";
|
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 {db} from "@/db";
|
||||||
import { eq } from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
// slug: string;
|
|
||||||
projectId: string
|
projectId: string
|
||||||
}>) {
|
}>) {
|
||||||
const {
|
const {
|
||||||
// slug: organizationSlug,
|
projectId
|
||||||
projectId } = await props.params;
|
} = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -33,41 +36,58 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
const proj = await db.query.project.findFirst({
|
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))),
|
where: (proj, {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
not
|
||||||
|
}) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
|
||||||
with: {
|
with: {
|
||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!proj) notFound();
|
if (!proj) {
|
||||||
|
redirect("/dashboard/projects");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
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">
|
||||||
{proj.name}
|
{capitalizeFirstLetter(proj.name)}
|
||||||
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/projects/${proj.id}/edit`}>
|
{!isMember && (
|
||||||
<GearIcon className="w-7 h-7" />
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
</Link>
|
href={`/dashboard/projects/${proj.id}/edit`}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
{!isMember && (
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
|
<PageActions className="justify-between">
|
||||||
</PageActions>
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
|
</PageActions>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageDescription>The list of associated databases</PageDescription>
|
<PageDescription>The list of associated databases</PageDescription>
|
||||||
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={proj.databases}
|
data={proj.databases}
|
||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
|
// @ts-ignore
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={4}
|
||||||
numberOfColumns={1}
|
numberOfColumns={1}
|
||||||
extendedProps={proj}
|
extendedProps={proj}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
|
<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>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
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 {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {DatabaseWith} from "@/db/schema/06_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization ) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
},
|
},
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
})
|
})
|
||||||
).filter((db) => db.project !== null) as DatabaseWith[];
|
).filter((db) => db.project == null) as DatabaseWith[];
|
||||||
|
|
||||||
const org = await db.query.organization.findFirst({
|
const org = await db.query.organization.findFirst({
|
||||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
||||||
@@ -35,6 +36,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
|
|
||||||
if (!org) notFound();
|
if (!org) notFound();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import {PageParams} from "@/types/next";
|
|||||||
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 {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
|
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -27,12 +34,14 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Projects</PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
{projects.length > 0 && (
|
{(projects.length > 0 && !isMember) && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={`/dashboard/projects/new`}>
|
<Link href={`/dashboard/projects/new`}>
|
||||||
<Button>+ Create Project</Button>
|
<Button>+ Create Project</Button>
|
||||||
@@ -41,17 +50,19 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="mt-10">
|
<PageContent>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
<CardsWithPagination
|
||||||
cardsPerPage={4} numberOfColumns={1}/>
|
organizationSlug={organization.slug}
|
||||||
|
data={projects}
|
||||||
|
cardItem={ProjectCard}
|
||||||
|
cardsPerPage={4}
|
||||||
|
numberOfColumns={1}
|
||||||
|
/>
|
||||||
|
) : isMember ? (
|
||||||
|
<EmptyStatePlaceholder text="No project available"/>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
|
||||||
href={`/dashboard/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>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
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 {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
|
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
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<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
|
||||||
}>) {
|
}>) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !users || !organization || organization.slug == "default") {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +30,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<OrganizationForm members={organization.members} defaultValues={organization}/>
|
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,42 +1,57 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {
|
import {
|
||||||
DeleteOrganizationButton
|
DeleteOrganizationButton
|
||||||
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
|
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
|
||||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
|
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
|
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
||||||
|
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Settings",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization || !activeMember) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notificationChannels = await getOrganizationChannels(organization.id)
|
||||||
|
|
||||||
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
Settings
|
Organization settings
|
||||||
{organization.slug != "default" ?
|
{permissions.canManageSettings && organization.slug !== "default" && (
|
||||||
<EditButtonSettings/>
|
<EditButtonSettings/>
|
||||||
: null}
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{organization.slug != "default" ?
|
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
||||||
: null}
|
)}
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageDescription>
|
|
||||||
Manage your organization settings.
|
|
||||||
</PageDescription>
|
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{/* TODO add the list of organisation members (add, remove, edit) */}
|
<OrganizationTabs
|
||||||
|
activeMember={activeMember}
|
||||||
|
organization={organization}
|
||||||
|
notificationChannels={notificationChannels}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
||||||
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
||||||
import { notFound } from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
import { asc, count, eq, inArray } from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
|
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export const metadata: Metadata = {
|
||||||
|
title: "Statistics",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
@@ -26,7 +32,13 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
where: eq(drizzleDb.schemas.project.organizationId, org.id),
|
where: eq(drizzleDb.schemas.project.organizationId, org.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectsCount = projects.length;
|
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({
|
const backupsEvolution = await db.query.backup.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
@@ -34,6 +46,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
||||||
|
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -44,56 +57,77 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
_count: count(),
|
_count: count(),
|
||||||
})
|
})
|
||||||
.from(drizzleDb.schemas.backup)
|
.from(drizzleDb.schemas.backup)
|
||||||
.where(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]))
|
.where(and(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]), inArray(drizzleDb.schemas.backup.databaseId, databaseIds)))
|
||||||
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
|
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
|
||||||
.orderBy(drizzleDb.schemas.backup.createdAt);
|
.orderBy(drizzleDb.schemas.backup.createdAt);
|
||||||
|
|
||||||
console.log(backupsRate);
|
|
||||||
|
const restorationsCountResult = await db
|
||||||
|
.select({
|
||||||
|
count: count(),
|
||||||
|
})
|
||||||
|
.from(drizzleDb.schemas.restoration)
|
||||||
|
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||||
|
|
||||||
|
|
||||||
|
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||||
|
const projectsCount = projects.length;
|
||||||
|
const backupsEvolutionCount = backupsEvolution.length;
|
||||||
|
|
||||||
|
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||||
|
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Statistics</PageTitle>
|
<PageTitle>Statistics Overview</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>Projects</CardTitle>
|
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||||
|
<Building2 className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>{projectsCount}</CardContent>
|
<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>
|
||||||
|
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>KPI 2</CardTitle>
|
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<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>
|
||||||
|
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>KPI 3</CardTitle>
|
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||||
|
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent></CardContent>
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{restorationsCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<Card className="w-full">
|
<EvolutionLineChart
|
||||||
<CardHeader>
|
data={sortedBackupsEvolution}
|
||||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
/>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
<PercentageLineChart
|
||||||
<EvolutionLineChart data={backupsEvolution} />
|
data={backupsRate}/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Success rate of backups</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<PercentageLineChart data={backupsRate} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</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,26 +1,26 @@
|
|||||||
import React from "react";
|
import React, {ReactNode} from "react";
|
||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
||||||
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
|
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
||||||
import {Header} from "@/features/layout/Header";
|
import {Header} from "@/features/layout/Header";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default async function Layout({children}: { children: ReactNode }) {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SidebarProvider>
|
||||||
<SidebarProvider>
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<ThemeMetaUpdater/>
|
||||||
<AppSidebar/>
|
<AppSidebar/>
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header/>
|
<Header/>
|
||||||
<main className="h-full">{children}</main>
|
<main className="h-full">{children}</main>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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 (
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
|
|
||||||
import {Badge} from "@/components/ui/badge";
|
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
|
||||||
import {getAccounts, getSessions} from "@/lib/auth/auth";
|
|
||||||
//import { getAccounts, getSessions } from "@/lib/auth/auth";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
const user = await currentUser();
|
|
||||||
console.log("my user",user);
|
|
||||||
if (!user) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// const sessions = await getSessions();
|
|
||||||
// const accounts = await getAccounts();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="justify-between gap-2 sm:flex">
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
<AvatarWithUpload
|
|
||||||
user={{
|
|
||||||
...user,
|
|
||||||
image: user.image ?? null,
|
|
||||||
role: user.role ?? null,
|
|
||||||
banned: user.banned ?? null,
|
|
||||||
banReason: user.banReason ?? null,
|
|
||||||
banExpires: user.banExpires ?? null,
|
|
||||||
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{user.name}
|
|
||||||
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
|
||||||
</PageTitle>
|
|
||||||
<PageActions className="mt-2 hidden sm:block">
|
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
|
||||||
<PageContent>
|
|
||||||
<UserForm
|
|
||||||
userId={user.id}
|
|
||||||
defaultValues={{
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role ?? undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="mt-4 sm:hidden">
|
|
||||||
<ButtonDeleteAccount text="Delete my account"/>
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ 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/${currentOrganizationSlug}/projects`);
|
redirect(`/dashboard/home`);
|
||||||
}
|
}
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
//Do not delete
|
//Do not delete
|
||||||
|
|||||||
@@ -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,12 +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 {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 {eventEmitter} from "../../../events/route";
|
import {eventEmitter} from "../../../events/route";
|
||||||
import {db} from "@/db";
|
|
||||||
import {Backup} from "@/db/schema/06_database";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
|
||||||
import * as drizzleDb from "@/db";
|
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,
|
||||||
@@ -25,6 +29,8 @@ export async function POST(
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -48,6 +54,10 @@ export async function POST(
|
|||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
alertPolicies: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -97,18 +107,36 @@ 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 (!aesKeyHex || !ivHex) {
|
||||||
|
return NextResponse.json({error: "Missing fields"}, {status: 400});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "File is required for successful backup"},
|
{error: "File is required for successful backup"},
|
||||||
{status: 400}
|
{status: 400}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const fileExtension = getFileExtension(database.dbms)
|
||||||
|
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
const fileName = `${uuid}.dump`;
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
|
||||||
|
|
||||||
const {success, message, filePath} = await uploadLocalPrivate(fileName, buffer);
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error("System settings not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let success: boolean, message: string, filePath: string;
|
||||||
|
|
||||||
|
const result =
|
||||||
|
settings.storage === "local"
|
||||||
|
? await uploadLocalPrivate(fileName, buffer)
|
||||||
|
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||||
|
|
||||||
|
({success, message, filePath} = result);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -119,14 +147,16 @@ export async function POST(
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({
|
.set(withUpdatedAt({
|
||||||
file: fileName,
|
file: fileName,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
})
|
}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.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",
|
||||||
@@ -137,11 +167,14 @@ export async function POST(
|
|||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({status: 'failed'})
|
.set(withUpdatedAt({status: 'failed'}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
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",
|
||||||
@@ -157,3 +190,4 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {eventEmitter} from "../../../events/route";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
@@ -13,7 +13,6 @@ export type BodyResultRestore = {
|
|||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{params}: { params: Promise<{ agentId: string }> }
|
||||||
@@ -26,8 +25,6 @@ export async function POST(
|
|||||||
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(
|
||||||
@@ -44,8 +41,10 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId)
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||||
|
with: {
|
||||||
|
alertPolicies: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -60,11 +59,15 @@ export async function POST(
|
|||||||
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 db
|
await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({ status: body.status as RestorationStatus })
|
.set({status: body.status as RestorationStatus})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -72,10 +75,7 @@ export async function POST(
|
|||||||
|
|
||||||
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,13 +1,17 @@
|
|||||||
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/07_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database} from "@/db/schema/06_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
|
import {SafeActionResult} from "next-safe-action";
|
||||||
|
import {ZodString} from "zod";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
@@ -35,17 +39,15 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
|
|
||||||
let backupAction: boolean = false
|
let backupAction: boolean = false
|
||||||
let restoreAction: boolean = false
|
let restoreAction: boolean = false
|
||||||
let UrlBackup: string = ""
|
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}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(db)
|
|
||||||
console.log(dbmsEnumSchema.parse(db.dbms))
|
|
||||||
const [databaseCreated] = await dbClient
|
const [databaseCreated] = await dbClient
|
||||||
.insert(drizzleDb.schemas.database)
|
.insert(drizzleDb.schemas.database)
|
||||||
.values({
|
.values({
|
||||||
@@ -58,62 +60,110 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup));
|
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [databaseUpdated] = await dbClient
|
const [databaseUpdated] = await dbClient
|
||||||
.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({ lastContact: lastContact })
|
.set(withUpdatedAt({
|
||||||
|
name: db.name,
|
||||||
|
agentId: agent.id,
|
||||||
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const backup = await dbClient.query.backup.findFirst({
|
const backup = await dbClient.query.backup.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status,"waiting"))
|
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const restoration = await dbClient.query.restoration.findFirst({
|
const restoration = await dbClient.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status,"waiting"))
|
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if(backup){
|
if (backup) {
|
||||||
backupAction = true
|
backupAction = true
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set({ status: "ongoing" })
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(restoration){
|
if (restoration) {
|
||||||
restoreAction = true
|
restoreAction = true
|
||||||
|
|
||||||
|
|
||||||
const backupToRestore = await dbClient.query.backup.findFirst({
|
const backupToRestore = await dbClient.query.backup.findFirst({
|
||||||
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
||||||
|
with: {
|
||||||
|
database: {
|
||||||
|
with: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find settings"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const fileName = backupToRestore?.file
|
const fileName = backupToRestore?.file
|
||||||
UrlBackup = await getFileUrlPresignedLocal(fileName ?? "")
|
|
||||||
|
|
||||||
|
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
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({ status: "ongoing" })
|
.set({status: "ongoing"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {db} from "@/db";
|
|||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -15,12 +16,13 @@ export type databaseAgent = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
|
version: string,
|
||||||
databases: databaseAgent[]
|
databases: databaseAgent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to test the get file url presigned local
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
const url = await getFileUrlPresignedLocal({fileName: "d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
|
||||||
return Response.json({
|
return Response.json({
|
||||||
message: url
|
message: url
|
||||||
})
|
})
|
||||||
@@ -34,29 +36,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
|
||||||
|
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUuidv4(agentId)) {
|
||||||
|
message = "agentId is not a valid uuid"
|
||||||
|
console.log(message)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{error: "agentId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
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 db
|
await db
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set({ lastContact: lastContact })
|
.set(withUpdatedAt({
|
||||||
|
version: body.version,
|
||||||
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
@@ -69,6 +78,7 @@ export async function POST(
|
|||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
console.log(response)
|
console.log(response)
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
|
|||||||
@@ -0,0 +1,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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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/files/";
|
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');
|
||||||
@@ -31,8 +31,8 @@ export async function GET(
|
|||||||
{status: 403}
|
{status: 403}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
const expiresAt = parseInt(expires, 10);
|
const expiresAt = parseInt(expires!, 10);
|
||||||
if (Date.now() > expiresAt) {
|
if (Date.now() > expiresAt) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Signed token expired'},
|
{error: 'Signed token expired'},
|
||||||
|
|||||||
@@ -1,30 +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 path from "path";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
|
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 {
|
try {
|
||||||
const fileName = (await params).fileName;
|
if (storageType === "local") {
|
||||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||||
|
|
||||||
// Check if the file exists
|
try {
|
||||||
try {
|
await fs.access(filePath);
|
||||||
await fs.access(filePath); // Ensures the file exists
|
const file = await fs.readFile(filePath);
|
||||||
} catch {
|
|
||||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file
|
const exists = await checkFileExistsInBucket({
|
||||||
const fileContent = await fs.readFile(filePath); // Returns a Buffer
|
bucketName: env.S3_BUCKET_NAME!,
|
||||||
|
fileName: `${privateS3ImageDir}${fileName}`,
|
||||||
|
});
|
||||||
|
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
|
||||||
|
|
||||||
return new NextResponse(fileContent, {
|
const nodeStream = await getObjectFromClient({
|
||||||
|
bucketName: env.S3_BUCKET_NAME!,
|
||||||
|
fileName: `${privateS3ImageDir}${fileName}`,
|
||||||
|
});
|
||||||
|
const webStream = nodeStreamToWebStream(nodeStream);
|
||||||
|
|
||||||
|
return new NextResponse(webStream, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Disposition": `attachment; filename="${fileName}"`,
|
"Content-Type": contentType,
|
||||||
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Disposition": `inline; filename="${fileName}"`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Error reading file:", error);
|
console.error("Error streaming image:", err);
|
||||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export type BodyInit = {
|
|
||||||
initialize: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const body: BodyInit = await request.json();
|
|
||||||
|
|
||||||
console.log(body);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Initialization successfully done!",
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in POST initialization:", error);
|
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,30 +1,37 @@
|
|||||||
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 inter = Inter({subsets: ["latin"]});
|
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
|
title: {
|
||||||
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
|
default: title,
|
||||||
|
template: `%s - ${title}`
|
||||||
|
},
|
||||||
|
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
<meta name="apple-mobile-web-app-title" content={title}/>
|
||||||
</head>
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body className={cn(inter.className, "h-full")}>
|
||||||
<Providers>{children}</Providers>
|
<ConsoleSilencer/>
|
||||||
|
<Providers>
|
||||||
|
{children}
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { redirect } from "next/navigation";
|
import BackButton from "@/components/wrappers/common/button/back-button";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
// redirect("/dashboard/profile");
|
|
||||||
return(
|
return(
|
||||||
<>
|
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||||
Error
|
<div className="w-full space-y-6 text-center">
|
||||||
</>
|
<div className="space-y-3">
|
||||||
|
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
|
||||||
|
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
|
||||||
|
</div>
|
||||||
|
<BackButton>Go home</BackButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PropsWithChildren, Suspense } 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 {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||||
|
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -12,9 +14,14 @@ const queryClient = new QueryClient();
|
|||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
>
|
||||||
|
<ThemeMetaUpdaterRoot/>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster />
|
<Toaster/>
|
||||||
{props.children}
|
{props.children}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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=${S3_ACCESS_KEY}
|
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
|
|||||||
@@ -1,77 +1,21 @@
|
|||||||
name: portabase-dev
|
name: portabase-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
db:
|
||||||
build:
|
image: postgres:16-alpine
|
||||||
context: .
|
ports:
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
- "5433:5432"
|
||||||
target: dev
|
volumes:
|
||||||
ports:
|
- postgres-data:/var/lib/postgresql/data
|
||||||
- "8887:80"
|
environment:
|
||||||
environment:
|
- POSTGRES_DB=devdb
|
||||||
- TIME_ZONE="Europe/Paris"
|
- POSTGRES_USER=devuser
|
||||||
- NODE_ENV=development
|
- POSTGRES_PASSWORD=changeme
|
||||||
depends_on:
|
healthcheck:
|
||||||
db:
|
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
|
||||||
condition: service_healthy
|
interval: 10s
|
||||||
volumes:
|
timeout: 5s
|
||||||
- .:/app
|
retries: 5
|
||||||
- /app/node_modules
|
|
||||||
container_name: portabase-app
|
|
||||||
|
|
||||||
develop:
|
|
||||||
watch:
|
|
||||||
- action: sync
|
|
||||||
path: .
|
|
||||||
target: /app
|
|
||||||
- action: rebuild
|
|
||||||
path: package.json
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
- POSTGRES_DB=devdb
|
|
||||||
- POSTGRES_USER=devuser
|
|
||||||
- POSTGRES_PASSWORD=changeme
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
db-pgadmin:
|
|
||||||
image: dpage/pgadmin4
|
|
||||||
environment:
|
|
||||||
PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
|
|
||||||
PGADMIN_DEFAULT_PASSWORD: "changeme"
|
|
||||||
PGADMIN_CONFIG_SERVER_MODE: "False"
|
|
||||||
POSTGRES_USER: "devuser"
|
|
||||||
POSTGRES_PASSWORD: "changeme"
|
|
||||||
volumes:
|
|
||||||
- pgadmin-data:/var/lib/pgadmin
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
s3:
|
|
||||||
image: docker.io/bitnami/minio:latest
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
pgadmin-data:
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
FROM node:23-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
ENV YARN_VERSION=4.9.1
|
ENV YARN_VERSION=4.9.1
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
|
tzdata \
|
||||||
build-base \
|
build-base \
|
||||||
libc6-compat \
|
libc6-compat \
|
||||||
openssl \
|
openssl \
|
||||||
@@ -82,16 +83,18 @@ COPY --from=builder /app/public ./public
|
|||||||
|
|
||||||
RUN mkdir .next
|
RUN mkdir .next
|
||||||
RUN chown nextjs:nodejs .next
|
RUN chown nextjs:nodejs .next
|
||||||
|
RUN mkdir -p /app/private/uploads
|
||||||
|
RUN chown -R nextjs:nodejs /app/private
|
||||||
|
RUN chown -R nextjs:nodejs /app/public
|
||||||
|
|
||||||
|
|
||||||
COPY --from=builder /app/next.config.ts ./
|
COPY --from=builder /app/next.config.ts ./
|
||||||
|
COPY --from=builder /app/portabase.config.ts ./
|
||||||
COPY --from=builder /app/drizzle.config.ts ./
|
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 src/db ./src/db
|
COPY --chown=nextjs:nodejs src/db ./src/db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||||
@@ -107,3 +110,4 @@ USER nextjs
|
|||||||
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +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
|
npx drizzle-kit generate
|
||||||
|
|
||||||
|
echo "▶ Applying migrations..."
|
||||||
npx drizzle-kit migrate
|
npx drizzle-kit migrate
|
||||||
|
|
||||||
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 v1.0.0 "
|
|
||||||
echo " "
|
|
||||||
|
|
||||||
|
|
||||||
node server.js
|
node server.js
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +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,25 +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,13 +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,
|
|
||||||
</Button>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +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,6 +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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export async function register() {
|
export async function register() {
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
const init = await import("@/utils/init");
|
const init = await import("@/utils/init");
|
||||||
init.init();
|
await init.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
|
||||||
import { errorHandler } from "@/middleware/errorHandler";
|
|
||||||
import { auth } from "@/lib/auth/auth";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
|
|
||||||
if (url.pathname.startsWith("/dashboard")) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: await headers(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.redirect(new URL("/login", request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: string) {
|
|
||||||
// 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\/images\/[^/]+\/?$/,
|
|
||||||
/^\/api\/events\/?$/,
|
|
||||||
/^\/api\/init\/?$/,
|
|
||||||
];
|
|
||||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
runtime: "nodejs",
|
|
||||||
matcher: [
|
|
||||||
// '/api/agent/:path*',
|
|
||||||
"/api/:path*",
|
|
||||||
"/dashboard/:path*",
|
|
||||||
"/dashboard",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// next-auth.d.ts
|
|
||||||
import NextAuth from "next-auth";
|
|
||||||
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
authMethod: string; // Add authMethod to the session user type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
authMethod: string; // Add authMethod to the user type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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,18 +1,84 @@
|
|||||||
import type { NextConfig } from "next";
|
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 = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
compiler: {
|
|
||||||
removeConsole: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
experimental: {
|
experimental: {
|
||||||
nodeMiddleware: true,
|
serverActions: {
|
||||||
|
bodySizeLimit: "50mb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,114 +1,122 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.1.0",
|
"version": "1.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 80",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
"build": "next build --experimental-build-mode compile",
|
"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:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:drop": "drizzle-kit drop",
|
"db:drop": "drizzle-kit drop",
|
||||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.10",
|
"@radix-ui/react-accordion": "^1.2.10",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
"@radix-ui/react-alert-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.9",
|
"@radix-ui/react-avatar": "^1.1.9",
|
||||||
"@radix-ui/react-checkbox": "^1.3.1",
|
"@radix-ui/react-checkbox": "^1.3.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.10",
|
"@radix-ui/react-collapsible": "^1.1.10",
|
||||||
"@radix-ui/react-context-menu": "^2.2.14",
|
"@radix-ui/react-context-menu": "^2.2.14",
|
||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.13",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-menubar": "^1.1.14",
|
"@radix-ui/react-menubar": "^1.1.14",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
"@radix-ui/react-navigation-menu": "^1.2.12",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-progress": "^1.1.6",
|
"@radix-ui/react-progress": "^1.1.6",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-radio-group": "^1.3.6",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-separator": "^1.1.6",
|
"@radix-ui/react-separator": "^1.1.6",
|
||||||
"@radix-ui/react-slider": "^1.3.4",
|
"@radix-ui/react-slider": "^1.3.4",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-switch": "^1.2.4",
|
"@radix-ui/react-switch": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.11",
|
"@radix-ui/react-tabs": "^1.1.11",
|
||||||
"@radix-ui/react-toast": "^1.2.13",
|
"@radix-ui/react-toast": "^1.2.13",
|
||||||
"@radix-ui/react-toggle": "^1.1.8",
|
"@radix-ui/react-toggle": "^1.1.8",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
"@radix-ui/react-toggle-group": "^1.1.9",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
"@react-email/components": "^0.0.41",
|
"@react-email/components": "^0.0.41",
|
||||||
"@t3-oss/env-nextjs": "^0.13.4",
|
"@t3-oss/env-nextjs": "^0.13.4",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.76.1",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"argon2": "^0.43.0",
|
"argon2": "^0.43.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "^1.2.8",
|
"better-auth": "1.4.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dockerode": "^4.0.6",
|
"dockerode": "^4.0.6",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"drizzle-zod": "^0.7.1",
|
"drizzle-zod": "^0.7.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "^15.4.2-canary.5",
|
"motion": "^12.23.24",
|
||||||
"next-safe-action": "^7.10.8",
|
"next": "16.0.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-safe-action": "^7.10.8",
|
||||||
"nodemailer": "^7.0.3",
|
"next-themes": "^0.4.6",
|
||||||
"npm-check-updates": "^18.0.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.0",
|
"node-forge": "^1.3.1",
|
||||||
"react": "19.1.0",
|
"nodemailer": "^7.0.3",
|
||||||
"react-day-picker": "9.7.0",
|
"npm-check-updates": "^18.0.1",
|
||||||
"react-dom": "19.1.0",
|
"pg": "^8.16.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react": "^19.2.0",
|
||||||
"react-email": "^4.0.13",
|
"react-day-picker": "9.7.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-dom": "^19.2.0",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-twc": "^1.4.2",
|
"react-email": "^4.0.13",
|
||||||
"recharts": "^2.15.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"socket.io": "^4.8.1",
|
"react-qr-code": "^2.0.18",
|
||||||
"socket.io-client": "^4.8.1",
|
"react-resizable-panels": "^3.0.2",
|
||||||
"sonner": "^2.0.3",
|
"react-twc": "^1.4.2",
|
||||||
"tailwind-merge": "^3.3.0",
|
"react-use-measure": "^2.1.7",
|
||||||
"uuid": "^11.1.0",
|
"recharts": "^2.15.3",
|
||||||
"vaul": "^1.1.2",
|
"socket.io": "^4.8.1",
|
||||||
"ws": "^8.18.2",
|
"socket.io-client": "^4.8.1",
|
||||||
"zod": "^3.24.4"
|
"sonner": "^2.0.3",
|
||||||
},
|
"tailwind-merge": "^3.3.0",
|
||||||
"devDependencies": {
|
"uuid": "^11.1.0",
|
||||||
"@iconify/react": "^6.0.0",
|
"vaul": "^1.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"ws": "^8.18.2",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"zod": "^3.24.4"
|
||||||
"@types/node": "^22.15.18",
|
},
|
||||||
"@types/pg": "^8.15.2",
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.4",
|
"@iconify/react": "^6.0.0",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@zenstackhq/openapi": "^2.14.2",
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"drizzle-kit": "^0.31.1",
|
"@types/node": "^22.15.18",
|
||||||
"eslint": "^9.26.0",
|
"@types/node-forge": "^1",
|
||||||
"eslint-config-next": "15.3.2",
|
"@types/pg": "^8.15.2",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
"@types/react": "^19.1.4",
|
||||||
"postcss": "^8.5.3",
|
"@types/react-dom": "^19.1.5",
|
||||||
"tailwindcss": "^4.1.7",
|
"@zenstackhq/openapi": "^2.14.2",
|
||||||
"tsx": "^4.19.4",
|
"@zenstackhq/tanstack-query": "^2.14.2",
|
||||||
"tw-animate-css": "^1.2.9",
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
"typescript": "^5.8.3",
|
"drizzle-kit": "^0.31.1",
|
||||||
"zenstack": "2.14.2"
|
"eslint": "^9.39.0",
|
||||||
},
|
"eslint-config-next": "^16.0.1",
|
||||||
"packageManager": "yarn@4.9.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"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.9.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface AuthProviderConfig {
|
||||||
|
id: "google" | "github" | "credential";
|
||||||
|
isActive: boolean;
|
||||||
|
icon: string;
|
||||||
|
isManual?: boolean;
|
||||||
|
credentials?: {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
|
||||||
|
{
|
||||||
|
id: "google",
|
||||||
|
icon: "hugeicons:chrome",
|
||||||
|
isActive: Boolean(process.env.AUTH_GOOGLE_METHOD),
|
||||||
|
// isManual: true,
|
||||||
|
credentials: {
|
||||||
|
clientId: process.env.AUTH_GOOGLE_ID || "",
|
||||||
|
clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "github",
|
||||||
|
icon: "iconoir:github",
|
||||||
|
isActive: Boolean(process.env.AUTH_GITHUB_METHOD),
|
||||||
|
credentials: {
|
||||||
|
clientId: process.env.AUTH_GITHUB_ID || "",
|
||||||
|
clientSecret: process.env.AUTH_GITHUB_SECRET || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "credential",
|
||||||
|
isActive: true,
|
||||||
|
icon: "proicons:key",
|
||||||
|
isManual: true,
|
||||||
|
credentials: {
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {NextRequest, NextResponse} from "next/server";
|
||||||
|
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
||||||
|
import {errorHandler} from "@/middleware/errorHandler";
|
||||||
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
import {headers} from "next/headers";
|
||||||
|
|
||||||
|
export async function proxy(request: NextRequest) {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/dashboard")) {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
|
||||||
|
}
|
||||||
|
if (session.user.banned) {
|
||||||
|
await auth.api.signOut({headers: await headers()});
|
||||||
|
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||||
|
}
|
||||||
|
if (session.user.role === "pending") {
|
||||||
|
await auth.api.signOut({headers: await headers()});
|
||||||
|
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
|
||||||
|
}
|
||||||
|
if (url.pathname === "/dashboard") {
|
||||||
|
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
|
||||||
|
}
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/api/auth")) {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/api")) {
|
||||||
|
const routeExists = checkRouteExists(url.pathname);
|
||||||
|
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 checkRouteExists(pathname: string) {
|
||||||
|
const routePatterns = [
|
||||||
|
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||||
|
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||||
|
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||||
|
/^\/api\/files\/[^/]+\/?$/,
|
||||||
|
/^\/api\/images\/[^/]+\/?$/,
|
||||||
|
/^\/api\/events\/?$/,
|
||||||
|
/^\/api\/init\/?$/,
|
||||||
|
/^\/api\/config\/?$/,
|
||||||
|
];
|
||||||
|
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
"/api/:path*",
|
||||||
|
"/dashboard/:path*",
|
||||||
|
"/dashboard",
|
||||||
|
]
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout from "../email-layout";
|
||||||
|
import {Heading, Text, Section, Button} from "@react-email/components";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
|
||||||
|
interface EmailCreateUserProps {
|
||||||
|
firstname?: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailForgotPassword = ({firstname, token}: EmailCreateUserProps) => {
|
||||||
|
const baseUrl = getServerUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmailLayout preview="Portabase - Forgot Password">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Hello <strong>{firstname}</strong>,
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
We received a request to reset the password for your account associated with this email.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
If you did not request this password reset, please ignore this email. Your current password
|
||||||
|
will remain unchanged. If this seems suspicious, please contact your administrator.
|
||||||
|
</Text>
|
||||||
|
<Section className="mt-[32px] mb-[32px] text-center">
|
||||||
|
<Button
|
||||||
|
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
|
||||||
|
href={`${baseUrl}/reset-password?token=${token}`}
|
||||||
|
>
|
||||||
|
Reset my password
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailForgotPassword;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout from "../email-layout";
|
||||||
|
import {Heading, Text} from "@react-email/components";
|
||||||
|
|
||||||
|
interface EmailCreateUserProps {
|
||||||
|
firstname?: string;
|
||||||
|
os: string;
|
||||||
|
browser: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailNewLogin = ({firstname, ipAddress, os, browser}: EmailCreateUserProps) => {
|
||||||
|
return (
|
||||||
|
<EmailLayout preview="Portabase - New Login">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Hello <strong>{firstname}</strong>,
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
We detected a new login to your account.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
<strong>Login details:</strong>
|
||||||
|
<br/>
|
||||||
|
Device: {os} - {browser}
|
||||||
|
<br/>
|
||||||
|
IP Address: {ipAddress}
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailNewLogin;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {Heading, Text, Section, Button} from "@react-email/components";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import EmailLayout from "@/components/emails/email-layout";
|
||||||
|
|
||||||
|
interface EmailCreateUserProps {
|
||||||
|
firstname?: string;
|
||||||
|
oldEmail?: string;
|
||||||
|
newEmail?: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailVerification = ({firstname, oldEmail, newEmail, url: urlVerification}: EmailCreateUserProps) => {
|
||||||
|
const serverUrl = new URL(getServerUrl());
|
||||||
|
const url = new URL(urlVerification);
|
||||||
|
url.hostname = serverUrl.hostname;
|
||||||
|
url.port = serverUrl.port == "80" ? "" : serverUrl.port;
|
||||||
|
const newUrl = url.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmailLayout preview="Portabase email verification">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Hello <strong>{firstname}</strong>,
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
We received a request to change the email address associated with your account.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
If you did not request this change, please ignore this email. Your current email address will remain
|
||||||
|
unchanged. If this seems suspicious, contact your administrator.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{oldEmail && newEmail ? (
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
<strong>Old email address:</strong> {oldEmail}
|
||||||
|
<br/>
|
||||||
|
<strong>New email address:</strong> {newEmail}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Section className="mt-[32px] mb-[32px] text-center">
|
||||||
|
<Button
|
||||||
|
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
|
||||||
|
href={newUrl}>
|
||||||
|
Confirm email address
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailVerification;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout from "./email-layout";
|
||||||
|
import {Heading, Text, Section, Button} from "@react-email/components";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
|
interface EmailCreateUserProps {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
||||||
|
const baseUrl = getServerUrl();
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmailLayout preview="Portabase Dashboard">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Your account on {env.PROJECT_NAME} has just been created!
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
<strong>Email: </strong>{email}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
<strong>Default password: </strong>{password}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mt-[32px] mb-[32px] text-center">
|
||||||
|
<Button
|
||||||
|
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
|
||||||
|
href={baseUrl}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailCreateUser;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 />
|
||||||
|
<Body className="mx-auto my-auto bg-white px-2 font-sans">
|
||||||
|
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
|
||||||
|
<Container className="mx-auto my-[40px] max-w-[465px] rounded border border-[#eaeaea] border-solid p-[20px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/images/logo.png`}
|
||||||
|
width="50"
|
||||||
|
height="auto"
|
||||||
|
alt="Logo"
|
||||||
|
className="mx-auto my-0"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
</Tailwind>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailLayout;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// import * as React from "react";
|
||||||
|
// import EmailLayout from "./email-layout";
|
||||||
|
// import {Text, Section, Button} from "@react-email/components";
|
||||||
|
// import type {EventPayload} from "@/features/notifications/types";
|
||||||
|
//
|
||||||
|
// export interface EmailNotificationProps {
|
||||||
|
// payload: EventPayload
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// export const EmailNotification = ({payload}: EmailNotificationProps) => {
|
||||||
|
// return (
|
||||||
|
// <EmailLayout preview={payload.title}>
|
||||||
|
// <Text className="text-base text-green-800 font-bold ">${payload.title}</Text>
|
||||||
|
// <Text className="text-base text-green-800 font-bold "><strong>Level:</strong> ${payload.level}</Text>
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// <Text className="text-base font-light text-green-800 ">
|
||||||
|
// ${payload.message.replace(/\n/g, '<br>')}
|
||||||
|
// </Text>{" "}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// <Section className="mt-[32px] mb-[32px] text-center">
|
||||||
|
//
|
||||||
|
// </Section>
|
||||||
|
//
|
||||||
|
// {payload.data && (
|
||||||
|
// <Section className="mt-[16px] text-sm text-gray-700">
|
||||||
|
// <pre>{JSON.stringify(payload.data, null, 2)}</pre>
|
||||||
|
// </Section>
|
||||||
|
// )}
|
||||||
|
//
|
||||||
|
// <Text className="text-base font-light text-green-800 ">Regards,<br/>Portabase</Text>
|
||||||
|
// </EmailLayout>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export default EmailNotification;
|
||||||
|
|
||||||
|
|
||||||
|
// const html = `
|
||||||
|
// <h2>${payload.title}</h2>
|
||||||
|
// <p><strong>Level:</strong> ${payload.level}</p>
|
||||||
|
// <p>${payload.message.replace(/\n/g, '<br>')}</p>
|
||||||
|
// ${payload.data ? `<pre>${JSON.stringify(payload.data, null, 2)}</pre>` : ''}
|
||||||
|
// `;
|
||||||
|
//
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout from "./email-layout";
|
||||||
|
import {Text, Section, Button} from "@react-email/components";
|
||||||
|
import type {EventPayload} from "@/features/notifications/types";
|
||||||
|
|
||||||
|
export interface EmailNotificationProps {
|
||||||
|
payload: EventPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmailNotification = ({payload}: EmailNotificationProps) => {
|
||||||
|
return (
|
||||||
|
<EmailLayout preview={payload.message}>
|
||||||
|
<Section>
|
||||||
|
<Text className="text-xl font-bold ">
|
||||||
|
{payload.title}
|
||||||
|
</Text>
|
||||||
|
<Text className=" font-semibold ">
|
||||||
|
Level: {payload.level}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{payload.message && (
|
||||||
|
<Section className="mb-2">
|
||||||
|
<Text
|
||||||
|
className="text-base font-light"
|
||||||
|
dangerouslySetInnerHTML={{__html: payload.message.replace(/\n/g, "<br>")}}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{payload.data && (
|
||||||
|
<Section className="mb-6 p-4 bg-gray-100 rounded text-sm text-gray-700">
|
||||||
|
<Text>
|
||||||
|
<pre>{JSON.stringify(payload.data, null, 2)}</pre>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Section className="mt-8">
|
||||||
|
<Text className="text-base font-light ">
|
||||||
|
Regards,
|
||||||
|
<br/>
|
||||||
|
Portabase
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailNotification;
|
||||||