Compare commits

..
Author SHA1 Message Date
charlesgauthereau 159398465f But on private and public dir permissions. 2025-07-18 11:31:24 +02:00
charlesgauthereau 76e5e55f14 Ready for release soon. 2025-07-17 13:22:56 +02:00
charlesgauthereau 605946abaa Ready for release soon. 2025-07-17 12:59:38 +02:00
charlesgauthereau 2890d6ac40 Ready for release soon. 2025-07-17 12:43:00 +02:00
charlesgauthereau ac3b4e424a Ready for release soon. 2025-07-17 12:05:39 +02:00
charlesgauthereau 8154f85eb6 Ready for release soon. 2025-07-17 12:01:11 +02:00
charlesgauthereau 45312207c9 Ready for release soon. 2025-07-17 11:58:13 +02:00
charlesgauthereau 128792645b Ready for release soon. 2025-07-17 11:50:27 +02:00
charlesgauthereau 4d52419247 Ready for release soon. 2025-07-17 09:28:57 +02:00
charlesgauthereau 7abdcd57f9 Ready for release soon. 2025-07-16 20:34:28 +02:00
charlesgauthereau 5e704ec18a Testing in local validated. 2025-07-16 20:16:18 +02:00
charlesgauthereau de9fe33194 Finish migration prisma to drizzle. 2025-07-16 18:15:28 +02:00
charlesgauthereau 385777535f Working on the organization system. 2025-07-16 12:36:11 +02:00
charlesgauthereau 405de7b323 Working on the Portabase debug. 2025-05-18 20:46:59 +02:00
Théo LAGACHE 6f2523e524 migration 2025-05-16 15:54:17 +02:00
charles-gauthereau 01019fe1a3 Patch force dynamic route for dashboard/agent 2025-02-21 08:59:37 +01:00
charles-gauthereau b1ecb7ce01 Bug. 2025-02-21 08:43:01 +01:00
charles-gauthereau 0cf350cdcc Bug. 2025-02-21 08:21:11 +01:00
charles-gauthereau ed5f7a43f2 Bug. 2025-02-20 10:26:37 +01:00
charles-gauthereau 54f0e87805 Deploy new version. 2025-02-20 09:26:38 +01:00
charles-gauthereau f570a67805 Deploy new version. 2025-02-14 09:29:47 +01:00
charles-gauthereau d0ec5902b5 Correcting some bugs in statistics. 2025-02-14 09:25:45 +01:00
charles-gauthereau 8586abf7e5 Correcting some bugs in statistics. 2025-02-13 19:20:55 +01:00
charles-gauthereau 7a59c616eb Adding some methods in order to support the serving files images dynamically in production. 2025-01-12 14:03:15 +01:00
charles-gauthereau 2440592286 Bug on Auth. 2025-01-12 12:30:32 +01:00
charles-gauthereau dca1799aac Bug on Auth. 2025-01-03 22:02:52 +01:00
charles-gauthereau d42d73661e url image bug patched. 2025-01-03 21:47:22 +01:00
charles-gauthereau a43878688b Patching some bugs. 2025-01-03 20:51:11 +01:00
charles-gauthereau 7ca905440c Patching some bugs. 2025-01-03 20:50:51 +01:00
charles-gauthereau c733fb4fdc Patching some bugs. 2025-01-03 20:48:58 +01:00
charles-gauthereau 7193f5b71f Patching some bugs. 2025-01-03 18:10:40 +01:00
charles-gauthereau b59a010425 Patching some bugs. 2025-01-03 15:55:16 +01:00
326 changed files with 16936 additions and 48134 deletions
+33
View File
@@ -0,0 +1,33 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Type (s3, local)
STORAGE_TYPE=local
+22 -2
View File
@@ -29,6 +29,16 @@ jobs:
with:
images: solucetechnologies/portabase
- name: Set tags
id: set-tags
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
TAGS="$TAGS,solucetechnologies/portabase:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
@@ -36,10 +46,20 @@ jobs:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.set-tags.outputs.tags }}
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
+1 -1
View File
@@ -4,7 +4,7 @@
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
#.yarn/install-state.gz
# testing
/coverage
+3
View File
@@ -0,0 +1,3 @@
{
"WillLuke.nextjs.hasPrompted": true
}
Binary file not shown.
-934
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,2 +1,2 @@
nodeLinker: "node-modules"
yarnPath: .yarn/releases/yarn-4.5.3.cjs
nodeLinker: node-modules
+1 -1
View File
@@ -1,2 +1,2 @@
migrate:
docker compose run --rm app sh -c "npx prisma migrate dev"
docker compose run --rm app
+158 -111
View File
@@ -1,21 +1,15 @@
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://portabase.io">
<img src="images/logo.png" alt="Logo" width="80" height="80">
</a>
[//]: # (<br />)
[//]: # (<div align="center">)
[//]: # ( <a href="https://github.com/othneildrew/Best-README-Template">)
[//]: # ( <img src="images/logo.png" alt="Logo" width="80" height="80">)
[//]: # ( </a>)
<h3 align="center">Portabase</h3>
<h3 align="center">Portabase</h3>
<p align="center">
Backup / Restore your databases instances
Easily backup and restore your database instances with Portabase
<br />
<a href="https://portabase.io"><strong>Explore the docs »</strong></a>
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
<br />
<br />
<a href="https://portabase.io">View Demo</a>
@@ -24,153 +18,206 @@
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</p>
</div>
## 📚 Table of Contents
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
- [Acknowledgments](#acknowledgments)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
---
<!-- ABOUT THE PROJECT -->
## About The Project
Portabase is the server dashboard service working with portabase agents.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
**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.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
### Built With
---
* [![NextJS][NextJS]][NextJS-url]
* [![Prisma][Prisma]][Prisma-url]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
## 🚀 Getting Started
### Prerequisites
Install docker on your machine
Ensure Docker is installed on your machine before getting started.
### Installation
Installation steps in order to use it locally and work on some features improvements
To run Portabase locally:
1. Clone the repo
```sh
git clone https://github.com/Soluce-Technologies/portabase
```
3. Start the project
```sh
docker compose up
```
1. Clone the repository:
```bash
git clone https://github.com/Soluce-Technologies/portabase
cd portabase
```
2. Start the development environment:
```bash
docker compose up
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
---
## 🛠️ Usage
<!-- ROADMAP -->
## Roadmap
Portabase provides a dashboard to manage database instances and backups. It supports:
- [ ] Add Changelog
- [ ] Improve documentation and repositories
- [ ] Add tests procedure
- [ ] Add Release File
- [ ] Security improvements
- [ ] Multi locale support
- [ ] Multi db support
- PostgreSQL (current)
- MongoDB (coming soon)
- MySQL (coming soon)
You can access the dashboard at `http://localhost:3000` after starting the project.
---
## 🗺️ Roadmap
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL
- [ ] MongoDB
- [ ] MySQL
See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues).
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
<!-- CONTRIBUTING -->
## Contributing
1. Fork the repository
2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Give the project a ⭐ if you like it!
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
### Top Contributors
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
[![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
### Top contributors:
---
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
</a>
## Developer Notes
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### Environment Variables
```yml
# Environment
NODE_ENV=production
## Memo
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format:
# Project Info
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)```
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)```
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
<!-- LICENSE -->
## License
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
```
Distributed under the Apache License. See `LICENSE.txt` for more information.
### Semantic Versioning
<p align="right">(<a href="#readme-top">back to top</a>)</p>
Use the following format for Docker image versioning:
```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
<!-- CONTACT -->
## Contact
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
Killian Larcher - killian.larcher@soluce-technologies.com
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
### Zenstack Support
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase)
To regenerate schema:
<p align="right">(<a href="#readme-top">back to top</a>)</p>
```bash
zenstack generate
```
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
---
## 📄 License
Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 📬 Contact
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
---
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Prisma]: https://img.shields.io/badge/Prisma-3982CE?style=for-the-badge&logo=Prisma&logoColor=white
[Prisma-url]: https://www.prisma.io/
[NextJS-url]: https://nextjs.org/
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
[NextJS-url]: https://nextjs.org/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
+1 -16
View File
@@ -1,24 +1,9 @@
import React from "react";
import {LayoutAdmin} from "@/components/layout";
import {currentUser} from "@/auth/current-user";
import {redirect} from "next/navigation";
import {prisma} from "@/prisma";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser()
if(user){
const userInfo = await prisma.user.findUnique({
where: {
email: user?.email
}
})
if(userInfo && userInfo.authMethod){
redirect('/')
// redirect('/dashboard')
}
}
return (
<LayoutAdmin>
+1 -1
View File
@@ -1,5 +1,5 @@
import {PageParams} from "@/types/next";
import {RegisterForm} from "@/components/wrappers/auth/Register/register-form/RegisterForm";
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
export default async function RoutePage(props: PageParams<{}>) {
return (
+20 -28
View File
@@ -1,42 +1,34 @@
import {PageParams} from "@/types/next";
import { PageParams } from "@/types/next";
import * as drizzleDb from "@/db";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import { 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 user = await requiredCurrentUser()
const users = await prisma.user.findMany({
where: {
// id: {
// not: user.id
// },
deleted: {not: true},
}
})
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
const settings = await db.query.setting.findFirst({
where: (fields, { eq }) => eq(fields.name, "system"),
});
return (
<Page>
<PageHeader>
<PageTitle>
Administration Panel
</PageTitle>
<PageTitle>Administration Panel</PageTitle>
</PageHeader>
<PageContent className="mt-10">
<AdminTabs settings={settings} currentUser={user} users={users}/>
<AdminTabs settings={settings!} users={users} />
</PageContent>
</Page>
)
}
);
}
@@ -1,19 +1,18 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import { notFound } from "next/navigation";
import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
export default async function RoutePage(props: PageParams<{
agentId: string;
}>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
}
const agent = await db.query.agent.findFirst({
where: (fields, { eq }) => eq(fields.id, agentId),
});
if (!agent) {
@@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{
return (
<Page>
<PageHeader>
<PageTitle>
Edit {agent.name}
</PageTitle>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id}/>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
)
}
);
}
@@ -1,6 +1,5 @@
import Link from "next/link";
import {GearIcon} from "@radix-ui/react-icons";
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {formatDateLastContact} from "@/utils/date-formatting";
@@ -9,38 +8,55 @@ import {Card, CardContent, CardHeader} from "@/components/ui/card";
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 {and, eq} from "drizzle-orm";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
},
include: {
databases: {}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true
}
})
//
console.log(agent)
const databaseId = 'db-123';
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
},
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
if (!agent) {
notFound()
}
//
// const databaseId = 'db-123';
//
// 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 (
<Page>
@@ -73,7 +89,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
Success rate
</CardHeader>
<CardContent>
{successRate ?? "Unavailable for now."}
{/*{successRate ?? "Unavailable for now."}*/}
</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
@@ -1,28 +1,16 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
import {currentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if(user.role != "admin"){
notFound()
}
return (
<Page>
<PageHeader>
<PageTitle>
Create new agent
</PageTitle>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm/>
<AgentForm />
</PageContent>
</Page>
)
}
);
}
@@ -1,22 +1,24 @@
import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import Link from 'next/link'
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import { PageParams } from "@/types/next";
import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { db } from "@/db";
export const dynamic = "force-dynamic";
export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany();
const agents = await prisma.agent.findMany()
if (!agents) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Agents
</PageTitle>
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
@@ -26,21 +28,17 @@ export default async function RoutePage(props: PageParams<{}>) {
)}
</PageHeader>
<PageContent className="mt-10">
{agents.length > 0 ?
<CardsWithPagination
data={agents}
cardItem={AgentCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
{agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
) : (
<Link
href={"/dashboard/agents/new"}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md"
>
Create new Agent
</Link>
}
)}
</PageContent>
</Page>
)
}
);
}
+9 -12
View File
@@ -1,16 +1,13 @@
import React from "react";
import {currentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import { notFound } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) {
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
const user = await currentUser()
if(user.role != "admin"){
notFound()
if (user!.role !== "superadmin" && user!.role !== "admin") {
notFound();
}
return (
<>
{children}
</>
)
}
return <>{children}</>;
}
@@ -0,0 +1,41 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/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>
);
}
@@ -0,0 +1,20 @@
"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>
);
}
@@ -0,0 +1,35 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const dbItem = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
});
if (!dbItem) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {dbItem.name}</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm
databaseId={databaseId}
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,75 @@
import {PageParams} from "@/types/next";
import {notFound} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
import {db} from "@/db";
import {eq, and} from "drizzle-orm";
import * as drizzleDb from "@/db";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const {databaseId} = await props.params;
const dbItem = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
});
if (!dbItem) {
notFound();
}
const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
with: {
restorations: true,
},
orderBy: (b, {desc}) => [desc(b.createdAt)],
});
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
orderBy: (r, {desc}) => [desc(r.createdAt)],
});
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const [totalBackups, successfulBackups] = await Promise.all([
db
.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
.then((rows) => rows.length),
db
.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(and(eq(drizzleDb.schemas.backup.databaseId, dbItem.id), eq(drizzleDb.schemas.backup.status, "success")))
.then((rows) => rows.length),
]);
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{dbItem.name}
<EditButton/>
<CronButton database={dbItem}/>
</PageTitle>
<PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi successRate={successRate} database={dbItem} totalBackups={totalBackups} />
<DatabaseTabs database={dbItem} isAlreadyRestore={isAlreadyRestore} backups={backups} restorations={restorations} />
</PageContent>
</Page>
);
}
@@ -0,0 +1,66 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {DatabaseWith} from "@/db/schema/06_database";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const {projectId} = await props.params;
const proj = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
with: {
databases: true,
},
});
if (!proj) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
);
// .filter((db): db is DatabaseWith => db.project !== null);
console.log("ici34",availableDatabases);
return (
<Page>
<PageHeader>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm
organization={org}
databases={availableDatabases as DatabaseWith[]}
defaultValues={{name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,74 @@
import { PageParams } from "@/types/next";
import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page";
import { buttonVariants } from "@/components/ui/button";
import { GearIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import { notFound } from "next/navigation";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
export default async function RoutePage(props: PageParams<{
// slug: string;
projectId: string
}>) {
const {
// slug: organizationSlug,
projectId } = await props.params;
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
const proj = await db.query.project.findFirst({
where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))),
with: {
databases: true,
},
});
if (!proj) notFound();
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{proj.name}
<Link className={buttonVariants({ variant: "outline" })} href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7" />
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"} />
</PageActions>
</div>
<PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? (
<CardsWithPagination
data={proj.databases}
organizationSlug={organization.slug}
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={proj}
/>
) : null}
</PageContent>
</Page>
);
}
@@ -0,0 +1,48 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {DatabaseWith} from "@/db/schema/06_database";
export default async function RoutePage(props: PageParams<{ }>) {
const organization = await getOrganization({});
if (!organization ) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project !== null) as DatabaseWith[];
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={org}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,59 @@
import Link from "next/link";
import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import {db} from "@/db";
import {notFound} from "next/navigation";
import {getOrganization} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{ }>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const projects = await db.query.project.findMany({
where: (project, {
eq,
and,
not
}) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))),
with: {
databases: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>Projects</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ? (
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
cardsPerPage={4} numberOfColumns={1}/>
) : (
<Link
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>
</Page>
);
}
@@ -0,0 +1,29 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
import {getOrganization} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{
}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm members={organization.members} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,43 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {currentUser} from "@/lib/auth/current-user";
import {getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({});
const user = await currentUser();
if (!organization) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Settings
{organization.slug != "default" ?
<EditButtonSettings/>
: null}
</PageTitle>
<PageActions>
{organization.slug != "default" ?
<DeleteOrganizationButton organizationSlug={organization.slug}/>
: null}
</PageActions>
</PageHeader>
<PageDescription>
Manage your organization settings.
</PageDescription>
<PageContent>
{/* TODO add the list of organisation members (add, remove, edit) */}
</PageContent>
</Page>
)
}
@@ -0,0 +1,101 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import { notFound } from "next/navigation";
import { db } from "@/db";
import { asc, count, eq, inArray } from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{ }>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
const projects = await db.query.project.findMany({
where: eq(drizzleDb.schemas.project.organizationId, org.id),
});
const projectsCount = projects.length;
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
});
const backupsRate = await db
.select({
createdAt: drizzleDb.schemas.backup.createdAt,
status: drizzleDb.schemas.backup.status,
_count: count(),
})
.from(drizzleDb.schemas.backup)
.where(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]))
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
.orderBy(drizzleDb.schemas.backup.createdAt);
console.log(backupsRate);
return (
<Page>
<PageHeader>
<PageTitle>Statistics</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>{projectsCount}</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
</CardHeader>
<CardContent></CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
</CardHeader>
<CardContent></CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution} />
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate} />
</CardContent>
</Card>
</div>
</PageContent>
</Page>
);
}
@@ -1,55 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
const databases = await prisma.database.findMany({
where: {
dbms: database.dbms,
}
})
const backups = await prisma.backup.findMany({
where: {
status: "success",
}
});
if (!database) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Restore {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
</PageContent>
</Page>
)
}
@@ -1,17 +0,0 @@
import {PageParams} from "@/types/next";
import {redirect} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
// return (
// <div className="flex flex-1 flex-col gap-4 p-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// )
}
@@ -1,39 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {DatabaseForm} from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
if (!database) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Edit {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
</PageContent>
</Page>
)
}
@@ -1,91 +0,0 @@
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {notFound} from "next/navigation";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi";
import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton";
import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const {databaseId} = await props.params
const database = await prisma.database.findUnique({
where: {
id: databaseId,
},
})
if (!database) {
notFound()
}
const backups = await prisma.backup.findMany({
where: {
databaseId: database.id
},
include:{
restorations: {}
},
orderBy: {
createdAt: "desc"
}
})
const restorations = await prisma.restoration.findMany({
where: {
databaseId: databaseId,
},
orderBy: {
createdAt: "desc"
}
})
const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting");
const isAlreadyRestore = !!restorations.find(restoration => restoration.status === "waiting");
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
},
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{database.name}
<EditButton/>
<CronButton database={database}/>
</PageTitle>
<PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi
successRate={successRate}
database={database}
totalBackups={totalBackups}
/>
<DatabaseTabs
database={database}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}
/>
</PageContent>
</Page>
)
}
@@ -1,61 +0,0 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
export default async function RoutePage(props: PageParams<{
projectId: string;
}>) {
const {projectId} = await props.params
const project = await prisma.project.findUnique({
where: {
id: projectId,
},
include: {
databases: {}
}
});
if (!project) {
notFound();
}
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
const availableDatabases = await prisma.database.findMany({
where: {
OR: [
{projectId: null},
{projectId: project.id},
],
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
return (
<Page>
<PageHeader>
<PageTitle>
Edit {project.name}
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project}
projectId={project.id}/>
</PageContent>
</Page>
)
}
@@ -1,76 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {prisma} from "@/prisma";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
const {slug: organizationSlug, projectId} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const project = await prisma.project.findUnique({
where: {
id: projectId,
},
include:{
databases: {}
}
})
if(!project){
notFound()
}
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{project.name}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject
projectId={projectId}
text={"Delete Project"}
/>
</PageActions>
</div>
<PageDescription>
The list of associated databases
</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{project.databases.length > 0 ?
<CardsWithPagination
data={project.databases}
organizationSlug={organizationSlug}
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={project}
/>
:
null
}
</PageContent>
</Page>
)
}
@@ -1,47 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
import {notFound} from "next/navigation";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const availableDatabases = await prisma.database.findMany({
where: {
projectId: null
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
const organization = await prisma.organization.findFirst({
where: {
slug: currentOrganizationSlug,
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Create new project
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={organization}/>
</PageContent>
</Page>
)
}
@@ -1,79 +0,0 @@
import Link from 'next/link'
import {PageParams} from "@/types/next";
import {prisma} from "@/prisma";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard";
import {requiredCurrentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {currentOrganization} from "@/auth/current-organization";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const projects = await prisma.project.findMany({
where: {
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
},
include: {
databases: {}
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Projects
</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/${currentOrganizationSlug}/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ?
<CardsWithPagination
organizationSlug={currentOrganizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link
href={`/dashboard/${currentOrganizationSlug}/projects/new`}
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
Create new Project
</Link>
}
</PageContent>
</Page>
)
}
@@ -1,62 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {prisma} from "@/prisma";
import {requiredCurrentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
export default async function RoutePage(props: PageParams<{
slug: string;
}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const user = await requiredCurrentUser()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const organization = await prisma.organization.findUnique({
where:{
slug: currentOrganizationSlug,
},
include:{
users:{
include:{
user:{
}
},
where: {
user: {
id: {
not: user.id, // Exclude user with id: 1
},
},
},
}
}
})
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
id: {not: user.id}
}
})
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -1,74 +0,0 @@
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const organization = await prisma.organization.findUnique({
where: {
slug: currentOrganizationSlug,
},
include: {
users:{
include:{
user: {}
}
}
}
})
const currentOrganizationUser = await prisma.userOrganization.findFirst({
where:{
userId: user.id,
organization:{
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
}
}
})
if (currentOrganizationUser.role != "admin") {
notFound()
}
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Settings
{organization.slug != "default" ?
<EditButtonSettings/>
: null}
</PageTitle>
<PageActions>
{organization.slug != "default" ?
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
: null}
</PageActions>
</PageHeader>
<PageDescription>
Manage your organization settings.
</PageDescription>
<PageContent>
<SettingsTabs settings={settings} currentUser={user} users={organization.users}/>
</PageContent>
</Page>
)
}
@@ -1,93 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
export default async function RoutePage(props: PageParams<{}>) {
const projectsCount = await prisma.project.count({
where: {
organization: {
slug: "default"
}
},
});
const backupsEvolution = await prisma.backup.findMany({
select: {
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
});
const backupsRate = await prisma.backup.groupBy({
by: ['createdAt', 'status'],
where: {
status: {
in: ['success', 'failed'],
},
},
_count: {
id: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>
Statistics
</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>
{projectsCount}
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
</div>
</PageContent>
</Page>
)
}
+7 -26
View File
@@ -1,45 +1,26 @@
import React from "react";
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 {currentUser} from "@/auth/current-user";
import {Header} from "@/features/layout/Header";
import {prisma} from "@/prisma";
import {GlobalStoreProvider} from "@/state-management/provider";
import {currentUser} from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser()
if (user) {
const userInfo = await prisma.user.findUnique({
where: {
email: user?.email
}
})
if (!userInfo) redirect('/login')
}
if (!user) redirect('/login')
const user = await currentUser();
if (!user) redirect("/login");
return (
// <HydrationZustand>
// <GlobalStoreProvider>
<>
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<AppSidebar/>
<SidebarInset>
<Header/>
<main className="h-full">
{children}
</main>
<main className="h-full">{children}</main>
</SidebarInset>
</div>
</SidebarProvider>
</>
// </GlobalStoreProvider>
// </HydrationZustand>
)
}
);
}
+3 -3
View File
@@ -1,9 +1,9 @@
import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner";
import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner";
export default function Loading() {
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50} />
</div>
)
}
);
}
+46 -26
View File
@@ -1,44 +1,64 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {requiredCurrentUser} from "@/auth/current-user";
import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm";
import {prisma} from "@/prisma";
import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload";
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 requiredCurrentUser()
const user = await currentUser();
console.log("my user",user);
if (!user) {
notFound()
return notFound();
}
const userInfo = await prisma.user.findUnique({
where: {
email: user.email
}
})
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={userInfo}/>
{user.name}
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
</PageTitle>
<PageActions className="mt-2 hidden sm:block">
<ButtonDeleteAccount text="Delete my account"/>
</PageActions>
</div>
<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={userInfo.id} defaultValues={userInfo}/>
<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" />
<ButtonDeleteAccount text="Delete my account"/>
</div>
</PageContent>
</Page>
)
}
);
}
+5 -5
View File
@@ -1,7 +1,7 @@
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-3xl font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
}
+7 -9
View File
@@ -1,16 +1,14 @@
import {redirect} from "next/navigation";
import {currentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { redirect } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { currentUser } from "@/lib/auth/current-user";
export default async function Index() {
const user = await currentUser()
const user = await currentUser();
if (user) {
const currentOrganizationSlug = await getCurrentOrganizationSlug()
redirect(`/dashboard/${currentOrganizationSlug}/projects`)
const currentOrganizationSlug = await getCurrentOrganizationSlug();
redirect(`/dashboard/${currentOrganizationSlug}/projects`);
}
redirect("/login")
redirect("/login");
//Do not delete
// return (
// <Home/>
+60 -52
View File
@@ -1,25 +1,27 @@
import {NextResponse} from "next/server";
import {prisma} from "@/prisma";
import {isUuidv4} from "@/utils/verify-uuid";
import {uploadLocalPrivate} from "@/features/upload/private/upload.action";
import {v4 as uuidv4} from "uuid";
import {Backup, Database} from "@prisma/client";
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";
export async function POST(
request: Request,
{ params }: { params: Promise<{ agentId: string }> }
{params}: { params: Promise<{ agentId: string }> }
) {
try {
const contentType = request.headers.get("Content-Type");
if (!contentType || !contentType.includes("multipart/form-data")) {
return NextResponse.json(
{ error: "Unsupported or missing Content-Type" },
{ status: 400 }
{error: "Unsupported or missing Content-Type"},
{status: 400}
);
}
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId;
const formData = await request.formData();
@@ -28,61 +30,64 @@ export async function POST(
if (!generatedId || !isUuidv4(generatedId)) {
return NextResponse.json(
{ error: "generatedId is not a valid UUID" },
{ status: 400 }
{error: "generatedId is not a valid UUID"},
{status: 400}
);
}
const agent = await prisma.agent.findFirst({
where: { id: agentId },
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
});
if (!agent) {
return NextResponse.json(
{ error: "Agent not found" },
{ status: 404 }
{error: "Agent not found"},
{status: 404}
);
}
const database = await prisma.database.findFirst({
where: { generatedId },
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
});
if (!database) {
return NextResponse.json(
{ error: "Database associated with generatedId not found" },
{ status: 404 }
{error: "Database associated with generatedId not found"},
{status: 404}
);
}
let backup: Backup | null = null;
let backup: Backup | null | undefined = null;
if (method === "automatic") {
backup = await prisma.backup.create({
data: {
status: "ongoing",
[backup] = await db
.insert(drizzleDb.schemas.backup)
.values({
status: 'ongoing',
databaseId: database.id,
},
});
})
.returning();
if (!backup) {
return NextResponse.json(
{ error: "Unable to create an automatic backup" },
{ status: 500 }
{error: "Unable to create an automatic backup"},
{status: 500}
);
}
} else {
backup = await prisma.backup.findFirst({
where: {
status: "ongoing",
databaseId: database.id,
},
backup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.status, 'ongoing'),
eq(drizzleDb.schemas.backup.databaseId, database.id),
),
});
if (!backup) {
return NextResponse.json(
{ error: "Unable to find the corresponding backup" },
{ status: 404 }
{error: "Unable to find the corresponding backup"},
{status: 404}
);
}
}
@@ -94,8 +99,8 @@ export async function POST(
if (!file) {
return NextResponse.json(
{ error: "File is required for successful backup" },
{ status: 400 }
{error: "File is required for successful backup"},
{status: 400}
);
}
@@ -103,49 +108,52 @@ export async function POST(
const fileName = `${uuid}.dump`;
const buffer = Buffer.from(await file.arrayBuffer());
const { success, message, filePath } = await uploadLocalPrivate(fileName, buffer);
const {success, message, filePath} = await uploadLocalPrivate(fileName, buffer);
if (!success) {
return NextResponse.json(
{ error: message },
{ status: 500 }
{error: message},
{status: 500}
);
}
await prisma.backup.update({
where: { id: backup.id },
data: {
await db
.update(drizzleDb.schemas.backup)
.set({
file: fileName,
status: "success",
},
});
eventEmitter.emit('modification', { update: true });
status: 'success',
})
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
{
message: "Backup successfully uploaded",
},
{ status: 200 }
{status: 200}
);
} else {
await prisma.backup.update({
where: { id: backup.id },
data: { status: "failed" },
});
eventEmitter.emit('modification', { update: true });
await db
.update(drizzleDb.schemas.backup)
.set({status: 'failed'})
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
{
message: "Backup successfully updated with status failed",
},
{ status: 200 }
{status: 200}
);
}
} catch (error) {
console.error("Error in POST handler:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
{error: "Internal server error"},
{status: 500}
);
}
}
+20 -26
View File
@@ -1,14 +1,17 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
import {prisma} from "@/prisma";
import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
export type BodyResultRestore = {
generatedId: string
status: string
}
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
export async function POST(
@@ -17,7 +20,7 @@ export async function POST(
) {
try {
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId
const body: BodyResultRestore = await request.json();
@@ -33,53 +36,44 @@ export async function POST(
);
}
const agent = await prisma.agent.findFirst({
where: {
id: agentId
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId)
})
if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404})
}
const database = await prisma.database.findFirst({
where: {
generatedId: body.generatedId
}
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId)
})
if (!database) {
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
}
const restoration = await prisma.restoration.findFirst({
where: {
status : "ongoing",
databaseId: database.id
}
const restoration = await db.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
})
if (!restoration) {
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
}
await prisma.restoration.update({
where:{
id : restoration.id
},
data: {
status: body.status
}
})
await db
.update(drizzleDb.schemas.restoration)
.set({ status: body.status as RestorationStatus })
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
const response = {
message: true,
details: "Restoration successfully updated"
}
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
return Response.json(response , {status: 200})
return Response.json(response, {status: 200})
} catch (error) {
+53 -55
View File
@@ -1,16 +1,19 @@
import {Agent, Database} from "@prisma/client";
import {prisma} from "@/prisma";
import {NextResponse} from "next/server";
import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
import {Agent} from "@/db/schema/07_agent";
import {Database} from "@/db/schema/06_database";
import * as drizzleDb from "@/db";
import {db as dbClient} from "@/db";
import {and, eq} from "drizzle-orm";
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
const databasesResponse = [];
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
generatedId: database.generatedId,
generatedId: database.agentDatabaseId,
dbms: database.dbms,
data: {
backup: {
@@ -25,15 +28,14 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
});
for (const db of body.databases) {
const existingDatabase = await prisma.database.findFirst({
where: {
generatedId: db.generatedId,
},
const existingDatabase = await dbClient.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
});
let backupAction: boolean = false
let restoreAction: boolean = false
let UrlBackup: string = null
let UrlBackup: string = ""
if (!existingDatabase) {
if (!isUuidv4(db.generatedId)) {
@@ -42,74 +44,70 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
{ status: 500 }
);
}
const databaseCreated = await prisma.database.create({
data: {
console.log(db)
console.log(dbmsEnumSchema.parse(db.dbms))
const [databaseCreated] = await dbClient
.insert(drizzleDb.schemas.database)
.values({
agentId: agent.id,
name: db.name,
dbms: db.dbms,
generatedId: db.generatedId,
dbms: db.dbms as EDbmsSchema,
agentDatabaseId: db.generatedId,
lastContact: lastContact,
},
});
})
.returning();
if (databaseCreated) {
databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup));
}
} else {
const databaseUpdated = await prisma.database.update({
where: {
id: existingDatabase.id,
},
data: {
lastContact: lastContact,
},
});
const backup = await prisma.backup.findFirst({
where: {
databaseId: databaseUpdated.id,
status: "waiting"
}
const [databaseUpdated] = await dbClient
.update(drizzleDb.schemas.database)
.set({ lastContact: lastContact })
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning();
const backup = await dbClient.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status,"waiting"))
})
const restoration = await prisma.restoration.findFirst({
where:{
databaseId: databaseUpdated.id,
status: "waiting"
}
const restoration = await dbClient.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status,"waiting"))
})
if(backup){
backupAction = true
await prisma.backup.update({
where:{
id: backup.id
},
data: {
status: "ongoing"
}
})
await dbClient
.update(drizzleDb.schemas.backup)
.set({ status: "ongoing" })
.where(eq(drizzleDb.schemas.backup.id, backup.id));
}
if(restoration){
restoreAction = true
const backupToRestore = await prisma.backup.findFirst({
where:{
id: restoration.backupId
}
})
const fileName = backupToRestore.file
UrlBackup = await getFileUrlPresignedLocal(fileName)
await prisma.restoration.update({
where: {
id: restoration.id
},
data:{
status: "ongoing"
}
const backupToRestore = await dbClient.query.backup.findFirst({
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
})
const fileName = backupToRestore?.file
UrlBackup = await getFileUrlPresignedLocal(fileName ?? "")
await dbClient
.update(drizzleDb.schemas.restoration)
.set({ status: "ongoing" })
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
}
+25 -17
View File
@@ -1,14 +1,16 @@
import {prisma} from "@/prisma";
import {NextResponse} from "next/server";
import {Dbms} from "@prisma/client";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
import {handleDatabases} from "./helpers";
import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {EDbmsSchema} from "@/db/schema/types";
import {eq} from "drizzle-orm";
import {isUuidv4} from "@/utils/verify-uuid";
export type databaseAgent = {
name: string,
dbms: Dbms,
dbms: EDbmsSchema,
generatedId: string
}
@@ -33,25 +35,31 @@ export async function POST(
const body: Body = await request.json();
const lastContact = new Date();
const agent = await prisma.agent.findFirst({
where: {
id: agentId
}
if (!isUuidv4(agentId)) {
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
);
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
})
if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact)
await prisma.agent.update({
where:{
id: agentId,
},
data:{
lastContact: lastContact,
}
})
eventEmitter.emit('modification', { update: true });
await db
.update(drizzleDb.schemas.agent)
.set({ lastContact: lastContact })
.where(eq(drizzleDb.schemas.agent.id, agentId));
eventEmitter.emit('modification', {update: true});
const response = {
agent: {
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
-4
View File
@@ -1,4 +0,0 @@
import {handlers} from "@/auth/auth";
export const {GET, POST} = handlers;
+2 -2
View File
@@ -12,7 +12,7 @@ export async function GET(
const expires = searchParams.get('expires');
const fileName = (await params).fileName
const privateLocalDir = "private/uploads/";
const privateLocalDir = "private/uploads/files/";
const filePath = path.join(privateLocalDir, fileName);
const crypto = require('crypto');
@@ -31,7 +31,7 @@ export async function GET(
{status: 403}
);
}
//@ts-ignore
const expiresAt = parseInt(expires, 10);
if (Date.now() > expiresAt) {
return NextResponse.json(
+30
View File
@@ -0,0 +1,30 @@
import path from "path";
import fs from "fs/promises";
import { NextResponse } from "next/server";
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
try {
const fileName = (await params).fileName;
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists
try {
await fs.access(filePath); // Ensures the file exists
} catch {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
// Read the file
const fileContent = await fs.readFile(filePath); // Returns a Buffer
return new NextResponse(fileContent, {
headers: {
"Content-Disposition": `attachment; filename="${fileName}"`,
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
},
});
} catch (error) {
console.error("Error reading file:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
+23
View File
@@ -0,0 +1,23 @@
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 });
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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

After

Width:  |  Height:  |  Size: 90 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+6 -6
View File
@@ -6,12 +6,11 @@ import "./globals.css";
import {Providers} from "./providers";
import {cn} from "@/lib/utils";
const inter = Inter({subsets: ["latin"]});
export const metadata: Metadata = {
title: "Portabase",
description: "Manage all your database instances from one place !",
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
};
export default function RootLayout({
@@ -21,10 +20,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="apple-mobile-web-app-title" content="Portabase"/>
</head>
<body className={cn(inter.className, "h-full")}>
<Providers>
{children}
</Providers>
<Providers>{children}</Providers>
</body>
</html>
);
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Portabase",
"short_name": "Portabase",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+9 -13
View File
@@ -1,14 +1,10 @@
"use client"
import {useRouter} from "next/navigation";
import {useEffect} from "react";
import { redirect } from "next/navigation";
export default function NotFound() {
const router = useRouter();
useEffect(() => {
router.push('/');
}, [router]);
return null; // You can return null or a loading spinner while the redirect happens
}
export default async function NotFound() {
// redirect("/dashboard/profile");
return(
<>
Error
</>
)
}
+12 -18
View File
@@ -1,29 +1,23 @@
"use client"
"use client";
import {PropsWithChildren} from "react";
import {ThemeProvider} from "@/features/theme/theme-provider";
import { PropsWithChildren, Suspense } from "react";
import { ThemeProvider } from "@/features/theme/theme-provider";
import {Toaster} from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider,} from '@tanstack/react-query'
import {SessionProvider} from "next-auth/react";
import { Toaster } from "@/components/ui/sonner";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient()
const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => {
return (
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
// disableTransitionOnChange
>
<Suspense fallback={null}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<Toaster/>
<Toaster />
{props.children}
</QueryClientProvider>
</ThemeProvider>
</SessionProvider>
)
}
</Suspense>
);
};
+3 -3
View File
@@ -45,9 +45,9 @@ services:
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=SoluceTechnologies
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
- MINIO_DEFAULT_BUCKETS=statics
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
volumes:
+68 -59
View File
@@ -1,68 +1,77 @@
name: portabase-dev
services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- "8887:80"
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: dev
ports:
- '8887:80'
environment:
- TIME_ZONE="Europe/Paris"
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=SoluceTechnologies
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
- MINIO_DEFAULT_BUCKETS=statics
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:
postgres-data:
minio_data:
postgres-data:
minio_data:
pgadmin-data:
+28 -15
View File
@@ -1,12 +1,22 @@
FROM node:23.5.0-alpine AS base
FROM node:23-alpine AS base
#ENV YARN_VERSION=4.2.2
ENV YARN_VERSION=4.5.3
ENV YARN_VERSION=4.9.1
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache openssl
RUN apk add --update --no-cache \
build-base \
libc6-compat \
openssl \
make \
g++ \
jpeg-dev \
cairo-dev \
giflib-dev \
pango-dev \
libtool \
autoconf \
automake \
libpng
# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
RUN yarn set version ${YARN_VERSION}
@@ -19,6 +29,7 @@ COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn; \
elif [ -f package-lock.json ]; then npm ci; \
@@ -49,10 +60,6 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npx zenstack generate
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
@@ -75,14 +82,19 @@ COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
RUN mkdir -p /app/private/uploads
RUN chown -R nextjs:nodejs /app/private
RUN chown -R nextjs:nodejs /app/public
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/schema.zmodel ./
#COPY --from=builder /app/src/lib/hooks ./src/lib/hooks
COPY --from=builder /app/src/openapi.yaml ./src/
COPY --from=builder /app/next.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/static ./.next/static
COPY --chown=nextjs:nodejs prisma ./prisma/
COPY --chown=nextjs:nodejs src/db ./src/db
USER root
@@ -98,3 +110,4 @@ ENV HOSTNAME="0.0.0.0"
USER nextjs
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
+2 -4
View File
@@ -3,10 +3,8 @@
npm install -g npm@11.0.0
npm -v
current_date=$(date +"%Y-%m-%d")
npx zenstack generate
npx prisma migrate dev --name $current_date
npx drizzle-kit generate
npx drizzle-kit migrate
npm run dev
+8 -3
View File
@@ -1,10 +1,15 @@
#!/bin/bash
npx install --save-dev @zenstackhq/openapi
echo " ____ __ __ "
echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
echo " "
echo " Community Edition v1.0.0 "
echo " "
npx zenstack generate
npx prisma migrate deploy
node server.js
exec "$@"
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "drizzle-kit";
import dotenv from "dotenv";
dotenv.config({
path: ".env",
});
export default defineConfig({
out: "./src/db/migrations",
schema: ["./src/db/schema", "./src/db/schema/types.ts"],
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
+4 -11
View File
@@ -1,13 +1,6 @@
import {
Button as ReactEmailButton
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
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}
/>
)
}
return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
};
+5 -27
View File
@@ -1,18 +1,7 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview,
Section,
Tailwind,
} from "@react-email/components";
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";
import { getServerUrl } from "@/utils/get-server-url";
const baseUrl = getServerUrl();
@@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
<Html>
<Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4"
style={{fontFamily: "Arial, sans-serif"}}>
<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>
<Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
<Section>{children}</Section>
</Container>
</Body>
</Html>
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?:
);
};
export default EmailLayout;
+3 -6
View File
@@ -5,12 +5,9 @@ import * as React from "react";
export default function HelloEmail() {
return (
<Html>
<Button
href="https://example.com"
style={{ background: "#000", color: "#fff" }}
>
Hello Charles
<Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
Hello,
</Button>
</Html>
);
}
}
+6 -32
View File
@@ -1,42 +1,16 @@
import {
Text,
} from "@react-email/components";
import { Text } from "@react-email/components";
import * as React from "react";
import EmailLayout from "./EmailLayout";
import {Button} from "@react-email/button";
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 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;
+4 -11
View File
@@ -1,13 +1,6 @@
import {
Text as ReactEmailText
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
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}
/>
)
}
return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
};
+4 -4
View File
@@ -1,6 +1,6 @@
import {init} from "@/utils/init";
export async function register() {
init()
if (process.env.NEXT_RUNTIME === "nodejs") {
const init = await import("@/utils/init");
init.init();
}
}
+37 -22
View File
@@ -1,55 +1,70 @@
import {NextRequest, NextResponse} from 'next/server'
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler";
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 function middleware(request: NextRequest) {
export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith('/api/auth')) {
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();
}
if (url.pathname.startsWith('/api')) {
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname);
// 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' } }
);
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
}
try{
try {
loggingMiddleware(request);
}catch(err){
errorHandler(err)
} catch (err) {
errorHandler(err);
}
}
// Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname) {
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\/[^/]+\/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));
return routePatterns.some((pattern) => pattern.test(pathname));
}
export const config = {
runtime: "nodejs",
matcher: [
// '/api/agent/:path*',
'/api/:path*',
"/api/:path*",
"/dashboard/:path*",
"/dashboard",
],
};
};
-28
View File
@@ -1,28 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
typescript: {
//TODO fix all type errors and remove 'ignoreBuildErrors'
// !! WARN !!
// Dangerously allow production builds to successfully complete even if
// your project has type errors.
// !! WARN !!
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
// bundlePagesRouterDependencies: true,
compiler: {
removeConsole: {
exclude: ['error'],
},
},
};
export default nextConfig;
+19
View File
@@ -0,0 +1,19 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
experimental: {
nodeMiddleware: true,
},
};
export default nextConfig;
+112 -103
View File
@@ -1,105 +1,114 @@
{
"name": "portabase",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 80",
"build": "next build",
"start": "next start",
"lint": "next lint",
"email": "email dev"
},
"dependencies": {
"@auth/prisma-adapter": "^2.7.4",
"@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.1.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.2",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-email/components": "^0.0.28",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.62.11",
"@tanstack/react-table": "^8.20.5",
"@types/ws": "^8.5.13",
"@zenstackhq/runtime": "2.10.2",
"argon2": "^0.41.1",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"dockerode": "^4.0.2",
"embla-carousel-react": "^8.3.1",
"input-otp": "^1.4.0",
"lucide-react": "^0.468.0",
"minio": "^8.0.2",
"next": "15.1.3",
"next-auth": "^5.0.0-beta.25",
"next-safe-action": "^7.10.2",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.16",
"npm-check-updates": "^17.1.13",
"react": "19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "19.0.0",
"react-email": "^3.0.2",
"react-hook-form": "^7.53.1",
"react-resizable-panels": "^2.1.6",
"react-twc": "^1.4.2",
"recharts": "^2.15.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"sonner": "^1.6.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3",
"vaul": "^1.1.1",
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/eslint-plugin-tailwindcss": "^3",
"@types/node": "^20",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@zenstackhq/openapi": "^2.10.2",
"@zenstackhq/tanstack-query": "^2.10.2",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"eslint-plugin-tailwindcss": "^3.17.5",
"postcss": "^8",
"prisma": "^6.1.0",
"tailwindcss": "^3.4.1",
"typescript": "^5",
"zenstack": "2.10.2"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
"name": "portabase",
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 80",
"build": "next build --experimental-build-mode compile",
"start": "next start",
"lint": "next lint",
"email": "email dev",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:drop": "drizzle-kit drop",
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-aspect-ratio": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-context-menu": "^2.2.14",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-toggle": "^1.1.8",
"@radix-ui/react-toggle-group": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.6",
"@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.4",
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-table": "^8.21.3",
"@types/nodemailer": "^6.4.17",
"@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.0",
"bcrypt": "^6.0.0",
"better-auth": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dockerode": "^4.0.6",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.510.0",
"minio": "^8.0.5",
"next": "^15.4.2-canary.5",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.3",
"npm-check-updates": "^18.0.1",
"pg": "^8.16.0",
"react": "19.1.0",
"react-day-picker": "9.7.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-email": "^4.0.13",
"react-hook-form": "^7.56.3",
"react-resizable-panels": "^3.0.2",
"react-twc": "^1.4.2",
"recharts": "^2.15.3",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.18.2",
"zod": "^3.24.4"
},
"devDependencies": {
"@iconify/react": "^6.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.15.18",
"@types/pg": "^8.15.2",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@zenstackhq/openapi": "^2.14.2",
"@zenstackhq/tanstack-query": "^2.14.2",
"drizzle-kit": "^0.31.1",
"eslint": "^9.26.0",
"eslint-config-next": "15.3.2",
"eslint-plugin-tailwindcss": "^3.18.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.7",
"tsx": "^4.19.4",
"tw-animate-css": "^1.2.9",
"typescript": "^5.8.3",
"zenstack": "2.14.2"
},
"packageManager": "yarn@4.9.1"
}
+1 -1
View File
@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
},
};
@@ -1,66 +0,0 @@
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"provider_account_id" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"session_token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verificationtokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"email_verified" TIMESTAMP(3),
"image" TEXT,
"role" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
-- CreateIndex
CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "password" TEXT;
@@ -1,46 +0,0 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('waiting', 'ongoing', 'failed', 'success');
-- CreateTable
CREATE TABLE "Agent" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Agent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Database" (
"id" TEXT NOT NULL,
"agent_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Database_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Backup" (
"id" TEXT NOT NULL,
"database_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Backup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Restore" (
"id" TEXT NOT NULL,
"backup_id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Restore_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "authMethod" TEXT;
@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Agent` will be added. If there are existing duplicate values, this will fail.
- Added the required column `slug` to the `Agent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Agent" ADD COLUMN "slug" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug");
-- AddForeignKey
ALTER TABLE "Database" ADD CONSTRAINT "Database_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,33 +0,0 @@
/*
Warnings:
- You are about to drop the column `authMethod` on the `users` table. All the data in the column will be lost.
- You are about to drop the `Restore` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "authMethod",
ADD COLUMN "auth_method" TEXT;
-- DropTable
DROP TABLE "Restore";
-- CreateTable
CREATE TABLE "Restauration" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "Restauration_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Backup" ADD CONSTRAINT "Backup_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "Backup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,13 +0,0 @@
-- CreateEnum
CREATE TYPE "TypeStorage" AS ENUM ('local', 's3');
-- CreateTable
CREATE TABLE "Settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
CONSTRAINT "Settings_pkey" PRIMARY KEY ("id")
);
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "s3EndPointUrl" TEXT;
@@ -1,6 +0,0 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "smtpFrom" TEXT,
ADD COLUMN "smtpHost" TEXT,
ADD COLUMN "smtpPassword" TEXT,
ADD COLUMN "smtpPort" TEXT,
ADD COLUMN "smtpUser" TEXT;
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `name` to the `Settings` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "name" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[name]` on the table `Settings` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Settings_name_key" ON "Settings"("name");
@@ -1,118 +0,0 @@
/*
Warnings:
- You are about to drop the `Agent` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Backup` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Database` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Restauration` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Settings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Backup" DROP CONSTRAINT "Backup_database_id_fkey";
-- DropForeignKey
ALTER TABLE "Database" DROP CONSTRAINT "Database_agent_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_backup_id_fkey";
-- DropForeignKey
ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_database_id_fkey";
-- DropTable
DROP TABLE "Agent";
-- DropTable
DROP TABLE "Backup";
-- DropTable
DROP TABLE "Database";
-- DropTable
DROP TABLE "Restauration";
-- DropTable
DROP TABLE "Settings";
-- CreateTable
CREATE TABLE "agents" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"last_contact" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "agents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "databases" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"backup_policy" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"agent_id" TEXT NOT NULL,
CONSTRAINT "databases_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "backups" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"file" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"database_id" TEXT NOT NULL,
CONSTRAINT "backups_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "restaurations" (
"id" TEXT NOT NULL,
"status" "Status" NOT NULL DEFAULT 'waiting',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"backup_id" TEXT NOT NULL,
"database_id" TEXT,
CONSTRAINT "restaurations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "settings" (
"id" TEXT NOT NULL,
"storage" "TypeStorage" NOT NULL DEFAULT 'local',
"name" TEXT NOT NULL,
"s3EndPointUrl" TEXT,
"s3AccessKeyId" TEXT,
"s3SecretAccessKey" TEXT,
"S3BucketName" TEXT,
"smtpPassword" TEXT,
"smtpFrom" TEXT,
"smtpHost" TEXT,
"smtpPort" TEXT,
"smtpUser" TEXT,
CONSTRAINT "settings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "agents_slug_key" ON "agents"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "settings_name_key" ON "settings"("name");
-- AddForeignKey
ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,11 +0,0 @@
/*
Warnings:
- Added the required column `dbms` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Dbms" AS ENUM ('postgresql', 'mysql', 'mongodb');
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "dbms" "Dbms" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "deleted" BOOLEAN;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "deleted" SET DEFAULT false;
@@ -1,12 +0,0 @@
/*
Warnings:
- Added the required column `role` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('pending', 'user', 'admin');
-- AlterTable
ALTER TABLE "users" DROP COLUMN "role",
ADD COLUMN "role" "Role" NOT NULL;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "last_contact" TIMESTAMP(3);
@@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `generatedId` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "generatedId" TEXT NOT NULL;
@@ -1,8 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[generatedId]` on the table `databases` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "databases_generatedId_key" ON "databases"("generatedId");
@@ -1,9 +0,0 @@
/*
Warnings:
- Added the required column `backupToRestore` to the `databases` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "databases" ADD COLUMN "backupToRestore" TEXT NOT NULL,
ADD COLUMN "isWaitingForBackup" BOOLEAN NOT NULL DEFAULT false;

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