Compare commits

...

11 Commits

Author SHA1 Message Date
Rostislav Dugin
02c735bc5a FIX (notifiers): Improve email validation 2025-11-14 18:02:27 +03:00
Rostislav Dugin
793b575146 FIX (storages): Ignore files removal errors for unavailable storage when deleting the database 2025-11-14 18:02:13 +03:00
Rostislav Dugin
a6e84b45f2 Merge pull request #84 from RostislavDugin/feature/add_pg_12
Feature/add pg 12
2025-11-12 15:43:09 +03:00
Rostislav Dugin
a941fbd093 FEATURE (postgres): Add PostgreSQL 12 tests and CI \ CD config 2025-11-12 15:39:44 +03:00
Rostislav Dugin
4492ba41f5 Merge pull request #82 from romanesko/feature/v12-support
feat: add PostgreSQL 12 support
2025-11-12 15:04:12 +03:00
Roman Bykovsky
3a5ac4b479 feat: add PostgreSQL 12 support 2025-11-11 18:53:26 +03:00
Rostislav Dugin
77aaabeaa1 FEATURE (docs): Update readme and docs links 2025-11-11 16:56:33 +03:00
Rostislav Dugin
01911dbf72 FIX (notifiers & storages): Avoid request for workspace_id for storages and notifiers removal 2025-11-11 10:05:45 +03:00
Rostislav Dugin
1a16f27a5d FIX (notifiers): Fix update of existing DB notifiers 2025-11-11 08:10:02 +03:00
Rostislav Dugin
778db71625 FIX (tests): Improve tests stability in CI \ CD 2025-11-09 20:41:36 +03:00
Rostislav Dugin
45fc9a7fff FIX (databases): Verify DB nil on side of DB instead of interface 2025-11-09 20:03:22 +03:00
37 changed files with 112 additions and 268 deletions

View File

@@ -127,6 +127,7 @@ jobs:
TEST_GOOGLE_DRIVE_CLIENT_SECRET=${{ secrets.TEST_GOOGLE_DRIVE_CLIENT_SECRET }}
TEST_GOOGLE_DRIVE_TOKEN_JSON=${{ secrets.TEST_GOOGLE_DRIVE_TOKEN_JSON }}
# testing DBs
TEST_POSTGRES_12_PORT=5000
TEST_POSTGRES_13_PORT=5001
TEST_POSTGRES_14_PORT=5002
TEST_POSTGRES_15_PORT=5003
@@ -154,6 +155,7 @@ jobs:
timeout 60 bash -c 'until docker exec dev-db pg_isready -h localhost -p 5437 -U postgres; do sleep 2; done'
# Wait for test databases
timeout 60 bash -c 'until nc -z localhost 5000; do sleep 2; done'
timeout 60 bash -c 'until nc -z localhost 5001; do sleep 2; done'
timeout 60 bash -c 'until nc -z localhost 5002; do sleep 2; done'
timeout 60 bash -c 'until nc -z localhost 5003; do sleep 2; done'
@@ -185,7 +187,7 @@ jobs:
- name: Run Go tests
run: |
cd backend
go test ./internal/...
go test -p=1 -count=1 -failfast ./internal/...
- name: Stop test containers
if: always()

View File

@@ -77,7 +77,7 @@ ENV APP_VERSION=$APP_VERSION
# Set production mode for Docker containers
ENV ENV_MODE=production
# Install PostgreSQL server and client tools (versions 13-17)
# Install PostgreSQL server and client tools (versions 12-18)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget ca-certificates gnupg lsb-release sudo gosu && \
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
@@ -85,7 +85,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
> /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
postgresql-17 postgresql-18 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-17 postgresql-18 postgresql-client-12 postgresql-client-13 postgresql-client-14 postgresql-client-15 \
postgresql-client-16 postgresql-client-17 postgresql-client-18 && \
rm -rf /var/lib/apt/lists/*

View File

@@ -9,7 +9,7 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/rostislavdugin/postgresus?color=brightgreen)](https://hub.docker.com/r/rostislavdugin/postgresus)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/RostislavDugin/postgresus)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-13%20%7C%2014%20%7C%2015%20%7C%2016%20%7C%2017%20%7C%2018-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-12%20%7C%2013%20%7C%2014%20%7C%2015%20%7C%2016%20%7C%2017%20%7C%2018-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/RostislavDugin/postgresus)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/RostislavDugin/postgresus)
@@ -40,13 +40,13 @@
- **Precise timing**: run backups at specific times (e.g., 4 AM during low traffic)
- **Smart compression**: 4-8x space savings with balanced compression (~20% overhead)
### 🗄️ **Multiple Storage Destinations**
### 🗄️ **Multiple Storage Destinations** <a href="https://postgresus.com/storages">(docs)</a>
- **Local storage**: Keep backups on your VPS/server
- **Cloud storage**: S3, Cloudflare R2, Google Drive, NAS, Dropbox and more
- **Secure**: All data stays under your control
### 📱 **Smart Notifications**
### 📱 **Smart Notifications** <a href="https://postgresus.com/notifiers">(docs)</a>
- **Multiple channels**: Email, Telegram, Slack, Discord, webhooks
- **Real-time updates**: Success and failure notifications
@@ -54,17 +54,24 @@
### 🐘 **PostgreSQL Support**
- **Multiple versions**: PostgreSQL 13, 14, 15, 16, 17 and 18
- **Multiple versions**: PostgreSQL 12, 13, 14, 15, 16, 17 and 18
- **SSL support**: Secure connections available
- **Easy restoration**: One-click restore from any backup
### 👥 **Suitable for Teams** <a href="https://postgresus.com/access-management">(docs)</a>
- **Workspaces**: Group databases, notifiers and storages for different projects or teams
- **Access management**: Control who can view or manage specific databases with role-based permissions
- **Audit logs**: Track all system activities and changes made by users
- **User roles**: Assign viewer, member, admin or owner roles within workspaces
### 🐳 **Self-Hosted & Secure**
- **Docker-based**: Easy deployment and management
- **Privacy-first**: All your data stays on your infrastructure
- **Open source**: Apache 2.0 licensed, inspect every line of code
### 📦 Installation
### 📦 Installation <a href="https://postgresus.com/installation">(docs)</a>
You have three ways to install Postgresus:
@@ -118,8 +125,6 @@ This single command will:
Create a `docker-compose.yml` file with the following configuration:
```yaml
version: "3"
services:
postgresus:
container_name: postgresus
@@ -149,9 +154,9 @@ docker compose up -d
6. **Add notifications** (optional): Configure email, Telegram, Slack, or webhook notifications
7. **Save and start**: Postgresus will validate settings and begin the backup schedule
### 🔑 Resetting Admin Password
### 🔑 Resetting Password <a href="https://postgresus.com/password">(docs)</a>
If you need to reset the admin password, you can use the built-in password reset command:
If you need to reset the password, you can use the built-in password reset command:
```bash
docker exec -it postgresus ./main --new-password="YourNewSecurePassword123" --email="admin"
@@ -169,4 +174,4 @@ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENS
## 🤝 Contributing
Contributions are welcome! Read [contributing guide](contribute/README.md) for more details, prioerities and rules are specified there. If you want to contribute, but don't know what and how - message me on Telegram [@rostislav_dugin](https://t.me/rostislav_dugin)
Contributions are welcome! Read <a href="https://postgresus.com/contributing">contributing guide</a> for more details, prioerities and rules are specified there. If you want to contribute, but don't know what and how - message me on Telegram [@rostislav_dugin](https://t.me/rostislav_dugin)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -17,6 +17,7 @@ TEST_GOOGLE_DRIVE_CLIENT_ID=
TEST_GOOGLE_DRIVE_CLIENT_SECRET=
TEST_GOOGLE_DRIVE_TOKEN_JSON="{\"access_token\":\"ya29..."
# testing DBs
TEST_POSTGRES_12_PORT=5000
TEST_POSTGRES_13_PORT=5001
TEST_POSTGRES_14_PORT=5002
TEST_POSTGRES_15_PORT=5003

View File

@@ -32,6 +32,17 @@ services:
command: server /data --console-address ":9001"
# Test PostgreSQL containers
test-postgres-12:
image: postgres:12
ports:
- "${TEST_POSTGRES_12_PORT}:5432"
environment:
- POSTGRES_DB=testdb
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpassword
container_name: test-postgres-12
shm_size: 1gb
test-postgres-13:
image: postgres:13
ports:

View File

@@ -33,6 +33,7 @@ type EnvVariables struct {
TestGoogleDriveClientSecret string `env:"TEST_GOOGLE_DRIVE_CLIENT_SECRET"`
TestGoogleDriveTokenJSON string `env:"TEST_GOOGLE_DRIVE_TOKEN_JSON"`
TestPostgres12Port string `env:"TEST_POSTGRES_12_PORT"`
TestPostgres13Port string `env:"TEST_POSTGRES_13_PORT"`
TestPostgres14Port string `env:"TEST_POSTGRES_14_PORT"`
TestPostgres15Port string `env:"TEST_POSTGRES_15_PORT"`
@@ -145,6 +146,10 @@ func loadEnvVariables() {
env.TempFolder = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "temp")
if env.IsTesting {
if env.TestPostgres12Port == "" {
log.Error("TEST_POSTGRES_12_PORT is empty")
os.Exit(1)
}
if env.TestPostgres13Port == "" {
log.Error("TEST_POSTGRES_13_PORT is empty")
os.Exit(1)

View File

@@ -516,7 +516,10 @@ func (s *BackupService) deleteBackup(backup *Backup) error {
err = storage.DeleteFile(backup.ID)
if err != nil {
return err
// we do not return error here, because sometimes clean up performed
// before unavailable storage removal or change - therefore we should
// proceed even in case of error
s.logger.Error("Failed to delete backup file", "error", err)
}
return s.backupRepository.DeleteByID(backup.ID)

View File

@@ -70,10 +70,10 @@ func (uc *CreatePostgresqlBackupUsecase) Execute(
"--verbose", // Add verbose output to help with debugging
}
// Use zstd compression level 5 for PostgreSQL 15+ (better compression and speed)
// Fall back to gzip compression level 5 for older versions
if pg.Version == tools.PostgresqlVersion13 || pg.Version == tools.PostgresqlVersion14 ||
pg.Version == tools.PostgresqlVersion15 {
// Use zstd compression level 5 for PostgreSQL 16+ (better compression and speed)
// Fall back to gzip compression level 5 for older versions (12-15)
if pg.Version == tools.PostgresqlVersion12 || pg.Version == tools.PostgresqlVersion13 ||
pg.Version == tools.PostgresqlVersion14 || pg.Version == tools.PostgresqlVersion15 {
args = append(args, "-Z", "5")
uc.logger.Info("Using gzip compression level 5 (zstd not available)", "version", pg.Version)
} else {

View File

@@ -94,7 +94,6 @@ func (c *BackupConfigController) GetBackupConfigByDbID(ctx *gin.Context) {
// @Tags backup-configs
// @Produce json
// @Param id path string true "Storage ID"
// @Param workspace_id query string true "Workspace ID"
// @Success 200 {object} map[string]bool
// @Failure 400
// @Failure 401
@@ -113,19 +112,7 @@ func (c *BackupConfigController) IsStorageUsing(ctx *gin.Context) {
return
}
workspaceIDStr := ctx.Query("workspace_id")
if workspaceIDStr == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "workspace_id query parameter is required"})
return
}
workspaceID, err := uuid.Parse(workspaceIDStr)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace_id"})
return
}
isUsing, err := c.backupConfigService.IsStorageUsing(user, workspaceID, id)
isUsing, err := c.backupConfigService.IsStorageUsing(user, id)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return

View File

@@ -341,7 +341,7 @@ func Test_IsStorageUsing_PermissionsEnforced(t *testing.T) {
test_utils.MakeGetRequestAndUnmarshal(
t,
router,
"/api/v1/backup-configs/storage/"+storage.ID.String()+"/is-using?workspace_id="+workspace.ID.String(),
"/api/v1/backup-configs/storage/"+storage.ID.String()+"/is-using",
"Bearer "+testUserToken,
tt.expectedStatusCode,
&response,
@@ -354,7 +354,7 @@ func Test_IsStorageUsing_PermissionsEnforced(t *testing.T) {
testResp := test_utils.MakeGetRequest(
t,
router,
"/api/v1/backup-configs/storage/"+storage.ID.String()+"/is-using?workspace_id="+workspace.ID.String(),
"/api/v1/backup-configs/storage/"+storage.ID.String()+"/is-using",
"Bearer "+testUserToken,
tt.expectedStatusCode,
)

View File

@@ -119,7 +119,6 @@ func (s *BackupConfigService) GetBackupConfigByDbId(
func (s *BackupConfigService) IsStorageUsing(
user *users_models.User,
workspaceID uuid.UUID,
storageID uuid.UUID,
) (bool, error) {
_, err := s.storageService.GetStorage(user, storageID)

View File

@@ -271,7 +271,6 @@ func (c *DatabaseController) TestDatabaseConnectionDirect(ctx *gin.Context) {
// @Tags databases
// @Produce json
// @Param id path string true "Notifier ID"
// @Param workspace_id query string true "Workspace ID"
// @Success 200 {object} map[string]bool
// @Failure 400
// @Failure 401
@@ -290,19 +289,7 @@ func (c *DatabaseController) IsNotifierUsing(ctx *gin.Context) {
return
}
workspaceIDStr := ctx.Query("workspace_id")
if workspaceIDStr == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "workspace_id query parameter is required"})
return
}
workspaceID, err := uuid.Parse(workspaceIDStr)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid workspace_id"})
return
}
isUsing, err := c.databaseService.IsNotifierUsing(user, workspaceID, id)
isUsing, err := c.databaseService.IsNotifierUsing(user, id)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return

View File

@@ -67,6 +67,10 @@ func (p *PostgresqlDatabase) TestConnection(logger *slog.Logger) error {
}
func (p *PostgresqlDatabase) HideSensitiveData() {
if p == nil {
return
}
p.Password = ""
}

View File

@@ -61,15 +61,13 @@ func (d *Database) TestConnection(logger *slog.Logger) error {
}
func (d *Database) HideSensitiveData() {
specificDB := d.getSpecificDatabase()
if specificDB != nil {
specificDB.HideSensitiveData()
}
d.getSpecificDatabase().HideSensitiveData()
}
func (d *Database) Update(incoming *Database) {
d.Name = incoming.Name
d.Type = incoming.Type
d.Notifiers = incoming.Notifiers
switch d.Type {
case DatabaseTypePostgres:

View File

@@ -219,7 +219,6 @@ func (s *DatabaseService) GetDatabasesByWorkspace(
func (s *DatabaseService) IsNotifierUsing(
user *users_models.User,
workspaceID uuid.UUID,
notifierID uuid.UUID,
) (bool, error) {
_, err := s.notifierService.GetNotifier(user, notifierID)

View File

@@ -69,6 +69,7 @@ func Test_BackupAndRestorePostgresql_RestoreIsSuccesful(t *testing.T) {
version string
port string
}{
{"PostgreSQL 12", "12", env.TestPostgres12Port},
{"PostgreSQL 13", "13", env.TestPostgres13Port},
{"PostgreSQL 14", "14", env.TestPostgres14Port},
{"PostgreSQL 15", "15", env.TestPostgres15Port},

View File

@@ -15,6 +15,7 @@ const (
type PostgresqlVersion string
const (
PostgresqlVersion12 PostgresqlVersion = "12"
PostgresqlVersion13 PostgresqlVersion = "13"
PostgresqlVersion14 PostgresqlVersion = "14"
PostgresqlVersion15 PostgresqlVersion = "15"
@@ -32,6 +33,8 @@ const (
func GetPostgresqlVersionEnum(version string) PostgresqlVersion {
switch version {
case "12":
return PostgresqlVersion12
case "13":
return PostgresqlVersion13
case "14":

View File

@@ -30,7 +30,7 @@ func GetPostgresqlExecutable(
return filepath.Join(basePath, executableName)
}
// VerifyPostgresesInstallation verifies that PostgreSQL versions 13-17 are installed
// VerifyPostgresesInstallation verifies that PostgreSQL versions 12-18 are installed
// in the current environment. Each version should be installed with the required
// client tools (pg_dump, psql) available.
// In development: ./tools/postgresql/postgresql-{VERSION}/bin
@@ -41,6 +41,7 @@ func VerifyPostgresesInstallation(
postgresesInstallDir string,
) {
versions := []PostgresqlVersion{
PostgresqlVersion12,
PostgresqlVersion13,
PostgresqlVersion14,
PostgresqlVersion15,

View File

@@ -5,7 +5,7 @@ set -e # Exit on any error
# Ensure non-interactive mode for apt
export DEBIAN_FRONTEND=noninteractive
echo "Installing PostgreSQL client tools versions 13-18 for Linux (Debian/Ubuntu)..."
echo "Installing PostgreSQL client tools versions 12-18 for Linux (Debian/Ubuntu)..."
echo
# Check if running on supported system
@@ -47,7 +47,7 @@ echo "Updating package list..."
$SUDO apt-get update -qq -y
# Install client tools for each version
versions="13 14 15 16 17 18"
versions="12 13 14 15 16 17 18"
for version in $versions; do
echo "Installing PostgreSQL $version client tools..."

5
backend/tools/download_macos.sh Normal file → Executable file
View File

@@ -2,7 +2,7 @@
set -e # Exit on any error
echo "Installing PostgreSQL client tools versions 13-18 for MacOS..."
echo "Installing PostgreSQL client tools versions 12-18 for MacOS..."
echo
# Check if Homebrew is installed
@@ -31,6 +31,7 @@ brew install wget openssl readline zlib
# PostgreSQL source URLs
declare -A PG_URLS=(
["12"]="https://ftp.postgresql.org/pub/source/v12.20/postgresql-12.20.tar.gz"
["13"]="https://ftp.postgresql.org/pub/source/v13.16/postgresql-13.16.tar.gz"
["14"]="https://ftp.postgresql.org/pub/source/v14.13/postgresql-14.13.tar.gz"
["15"]="https://ftp.postgresql.org/pub/source/v15.8/postgresql-15.8.tar.gz"
@@ -107,7 +108,7 @@ build_postgresql_client() {
}
# Build each version
versions="13 14 15 16 17 18"
versions="12 13 14 15 16 17 18"
for version in $versions; do
url=${PG_URLS[$version]}

View File

@@ -1,7 +1,7 @@
@echo off
setlocal enabledelayedexpansion
echo Downloading and installing PostgreSQL versions 13-18 for Windows...
echo Downloading and installing PostgreSQL versions 12-18 for Windows...
echo.
:: Create downloads and postgresql directories if they don't exist
@@ -17,6 +17,7 @@ cd downloads
set "BASE_URL=https://get.enterprisedb.com/postgresql"
:: Define versions and their corresponding download URLs
set "PG12_URL=%BASE_URL%/postgresql-12.20-1-windows-x64.exe"
set "PG13_URL=%BASE_URL%/postgresql-13.16-1-windows-x64.exe"
set "PG14_URL=%BASE_URL%/postgresql-14.13-1-windows-x64.exe"
set "PG15_URL=%BASE_URL%/postgresql-15.8-1-windows-x64.exe"
@@ -25,7 +26,7 @@ set "PG17_URL=%BASE_URL%/postgresql-17.0-1-windows-x64.exe"
set "PG18_URL=%BASE_URL%/postgresql-18.0-1-windows-x64.exe"
:: Array of versions
set "versions=13 14 15 16 17 18"
set "versions=12 13 14 15 16 17 18"
:: Download and install each version
for %%v in (%versions%) do (

View File

@@ -1,6 +1,6 @@
This directory is needed only for development and CI\CD.
We have to download and install all the PostgreSQL versions from 13 to 18 locally.
We have to download and install all the PostgreSQL versions from 12 to 18 locally.
This is needed so we can call pg_dump, pg_dumpall, etc. on each version of the PostgreSQL database.
You do not need to install PostgreSQL fully with all the components.
@@ -8,6 +8,7 @@ We only need the client tools (pg_dump, pg_dumpall, psql, etc.) for each version
We have to install the following:
- PostgreSQL 12
- PostgreSQL 13
- PostgreSQL 14
- PostgreSQL 15
@@ -72,6 +73,7 @@ The final directory structure should match:
For example:
- `./tools/postgresql/postgresql-12/bin/pg_dump`
- `./tools/postgresql/postgresql-13/bin/pg_dump`
- `./tools/postgresql/postgresql-14/bin/pg_dump`
- `./tools/postgresql/postgresql-15/bin/pg_dump`

View File

@@ -1,94 +0,0 @@
### Prerequisites
1. Read docs in /docs folder, README.md in /backend and /frontend folders
2. Run both backend and frontend following the instructions in their respective README.md files (for development)
3. Read this file till the end
### How to create a pull request?
We use gitflow approach.
1. Create a new branch from main
2. Make changes
3. Create a pull request to main
4. Wait for review
5. Merge pull request
Commits should be named in the following format depending on the type of change:
- `FEATURE (area): What was done`
- `FIX (area): What was fixed`
- `REFACTOR (area): What was refactored`
To see examples, look at commit history in main branch.
Branches should be named in the following format:
- `feature/what_was_done`
- `fix/what_was_fixed`
- `refactor/what_was_refactored`
Example:
- `feature/add_support_of_kubernetes_helm`
- `fix/make_healthcheck_optional`
- `refactor/refactor_navbar`
Before any commit, make sure:
1. You created critical tests for your changes
2. `make lint` is passing (for backend) and `npm run lint` is passing (for frontend)
3. All tests are passing
4. Project is building successfully
5. All your commits should be squashed into one commit with proper message (or to meaningful parts)
6. Code do really refactored and production ready
7. You have one single PR per one feature (at least, if features not connected)
### Automated Versioning
This project uses automated versioning based on commit messages:
- **FEATURE (area)**: Creates a **minor** version bump (e.g., 1.0.0 → 1.1.0)
- **FIX (area)**: Creates a **patch** version bump (e.g., 1.0.0 → 1.0.1)
- **REFACTOR (area)**: Creates a **patch** version bump (e.g., 1.0.0 → 1.0.1)
- **BREAKING CHANGE**: Creates a **major** version bump (e.g., 1.0.0 → 2.0.0)
The system automatically:
- Analyzes commits since the last release
- Determines the appropriate version bump
- Generates a changelog grouped by area (frontend/backend/etc.)
- Creates GitHub releases with detailed release notes
- Updates package.json version numbers
To skip automated release (for documentation updates, etc.), add `[skip-release]` to your commit message.
### Docs
If you need to add some explanation, do it in appropriate place in the code. Or in the /docs folder if it is something general. For charts, use Mermaid.
### Priorities
Before taking anything more than a couple of lines of code, please write Rostislav via Telegram (@rostislav_dugin) and confirm priority. It is possible that we already have something in the works, it is not needed or it's not project priority.
Nearsest features:
- add API keys and API actions
- add encryption
Storages tasks:
- check AWS S3 support
- check Google Cloud S3 support
- add FTP
- add Dropbox
- add OneDrive
- add NAS
- add Yandex Drive
Notifications tasks:
- add Mattermost
- make webhooks flexible
- add Gotify
Extra:
- add HTTPS for Postgresus

View File

@@ -1,45 +0,0 @@
# How to add new notifier to Postgresus (Discord, Slack, Telegram, Email, Webhook, etc.)
## Backend part
1. Create new model in `backend/internal/features/notifiers/models/{notifier_name}/` folder. Implement `NotificationSender` interface from parent folder.
- The model should implement `Send(logger *slog.Logger, heading string, message string) error` and `Validate() error` methods
- Use UUID primary key as `NotifierID` that references the main notifiers table
2. Add new notifier type to `backend/internal/features/notifiers/enums.go` in the `NotifierType` constants.
3. Update the main `Notifier` model in `backend/internal/features/notifiers/model.go`:
- Add new notifier field with GORM foreign key relation
- Update `getSpecificNotifier()` method to handle the new type
- Update `Send()` method to route to the new notifier
4. If you need to add some .env variables to test, add them in `backend/internal/config/config.go` (so we can use it in tests)
5. If you need some Docker container to test, add it to `backend/docker-compose.yml.example`. For sensitive data - keep it blank.
6. If you need some sensitive envs to test in pipeline, message @rostislav_dugin so I can add it to GitHub Actions. For example, API keys or credentials.
7. Create new migration in `backend/migrations` folder:
- Create table with `notifier_id` as UUID primary key
- Add foreign key constraint to `notifiers` table with CASCADE DELETE
- Look at existing notifier migrations for reference
8. Make sure that all tests are passing.
## Frontend part
If you are able to develop only backend - it's fine, message @rostislav_dugin so I can complete UI part.
1. Add models and validator to `frontend/src/entity/notifiers/models/{notifier_name}/` folder and update `index.ts` file to include new model exports.
2. Upload an SVG icon to `public/icons/notifiers/`, update `src/entity/notifiers/models/getNotifierLogoFromType.ts` to return new icon path, update `src/entity/notifiers/models/NotifierType.ts` to include new type, and update `src/entity/notifiers/models/getNotifierNameFromType.ts` to return new name.
3. Add UI components to manage your notifier:
- `src/features/notifiers/ui/edit/notifiers/Edit{NotifierName}Component.tsx` (for editing)
- `src/features/notifiers/ui/show/notifier/Show{NotifierName}Component.tsx` (for display)
4. Update main components to handle the new notifier type:
- `EditNotifierComponent.tsx` - add import, validation function, and component rendering
- `ShowNotifierComponent.tsx` - add import and component rendering
5. Make sure everything is working as expected.

View File

@@ -1,51 +0,0 @@
# How to add new storage to Postgresus (S3, FTP, Google Drive, NAS, etc.)
## Backend part
1. Create new model in `backend/internal/features/storages/models/{storage_name}/` folder. Implement `StorageFileSaver` interface from parent folder.
- The model should implement `SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.Reader) error`, `GetFile(fileID uuid.UUID) (io.ReadCloser, error)`, `DeleteFile(fileID uuid.UUID) error`, `Validate() error`, and `TestConnection() error` methods
- Use UUID primary key as `StorageID` that references the main storages table
- Add `TableName() string` method to return the proper table name
2. Add new storage type to `backend/internal/features/storages/enums.go` in the `StorageType` constants.
3. Update the main `Storage` model in `backend/internal/features/storages/model.go`:
- Add new storage field with GORM foreign key relation
- Update `getSpecificStorage()` method to handle the new type
- Update `SaveFile()`, `GetFile()`, and `DeleteFile()` methods to route to the new storage
- Update `Validate()` method to include new storage validation
4. If you need to add some .env variables to test, add them in `backend/internal/config/config.go` (so we can use it in tests)
5. If you need some Docker container to test, add it to `backend/docker-compose.yml.example`. For sensitive data - keep it blank.
6. If you need some sensitive envs to test in pipeline, message @rostislav_dugin so I can add it to GitHub Actions. For example, Google Drive envs or FTP credentials.
7. Create new migration in `backend/migrations` folder:
- Create table with `storage_id` as UUID primary key
- Add foreign key constraint to `storages` table with CASCADE DELETE
- Look at existing storage migrations for reference
8. Update tests in `backend/internal/features/storages/model_test.go` to test new storage
9. Make sure that all tests are passing.
## Frontend part
If you are able to develop only backend - it's fine, message @rostislav_dugin so I can complete UI part.
1. Add models and api to `frontend/src/entity/storages/models/` folder and update `index.ts` file to include new model exports.
- Create TypeScript interface for your storage model
- Add validation function if needed
2. Upload an SVG icon to `public/icons/storages/`, update `src/entity/storages/models/getStorageLogoFromType.ts` to return new icon path, update `src/entity/storages/models/StorageType.ts` to include new type, and update `src/entity/storages/models/getStorageNameFromType.ts` to return new name.
3. Add UI components to manage your storage:
- `src/features/storages/ui/edit/storages/Edit{StorageName}Component.tsx` (for editing)
- `src/features/storages/ui/show/storages/Show{StorageName}Component.tsx` (for display)
4. Update main components to handle the new storage type:
- `EditStorageComponent.tsx` - add import and component rendering
- `ShowStorageComponent.tsx` - add import and component rendering
5. Make sure everything is working as expected.

View File

@@ -1,4 +1,5 @@
export enum PostgresqlVersion {
PostgresqlVersion12 = '12',
PostgresqlVersion13 = '13',
PostgresqlVersion14 = '14',
PostgresqlVersion15 = '15',

View File

@@ -1,7 +1,8 @@
import { FormValidator } from '../../../../shared/lib';
import type { EmailNotifier } from './EmailNotifier';
export const validateEmailNotifier = (isCreate: boolean, notifier: EmailNotifier): boolean => {
if (!notifier.targetEmail) {
if (!notifier.targetEmail || !FormValidator.isValidEmail(notifier.targetEmail)) {
return false;
}
@@ -17,5 +18,9 @@ export const validateEmailNotifier = (isCreate: boolean, notifier: EmailNotifier
return false;
}
if (notifier.smtpUser && !FormValidator.isValidEmail(notifier.smtpUser)) {
return false;
}
return true;
};

View File

@@ -175,6 +175,7 @@ export const BackupsComponent = ({ database, isCanManageDBs, scrollContainerRef
try {
await backupsApi.makeBackup(database.id);
await new Promise((resolve) => setTimeout(resolve, 1000));
setCurrentLimit(BACKUPS_PAGE_SIZE);
setHasMore(true);
await loadBackups(BACKUPS_PAGE_SIZE);

View File

@@ -128,6 +128,7 @@ export const EditDatabaseSpecificDataComponent = ({
className="max-w-[200px] grow"
placeholder="Select PG version"
options={[
{ label: '12', value: PostgresqlVersion.PostgresqlVersion12 },
{ label: '13', value: PostgresqlVersion.PostgresqlVersion13 },
{ label: '14', value: PostgresqlVersion.PostgresqlVersion14 },
{ label: '15', value: PostgresqlVersion.PostgresqlVersion15 },

View File

@@ -5,6 +5,7 @@ interface Props {
}
const postgresqlVersionLabels = {
[PostgresqlVersion.PostgresqlVersion12]: '12',
[PostgresqlVersion.PostgresqlVersion13]: '13',
[PostgresqlVersion.PostgresqlVersion14]: '14',
[PostgresqlVersion.PostgresqlVersion15]: '15',

View File

@@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import { Input, Tooltip } from 'antd';
import type { Notifier } from '../../../../../entity/notifiers';
import { FormValidator } from '../../../../../shared/lib';
interface Props {
notifier: Notifier;
@@ -31,6 +32,12 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
size="small"
className="w-full max-w-[250px]"
placeholder="example@gmail.com"
status={
notifier?.emailNotifier?.targetEmail &&
!FormValidator.isValidEmail(notifier.emailNotifier.targetEmail)
? 'error'
: undefined
}
/>
<Tooltip className="cursor-pointer" title="The email where you want to receive the message">
@@ -102,6 +109,12 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
size="small"
className="w-full max-w-[250px]"
placeholder="user@gmail.com"
status={
notifier?.emailNotifier?.smtpUser &&
!FormValidator.isValidEmail(notifier.emailNotifier.smtpUser)
? 'error'
: undefined
}
/>
</div>

View File

@@ -14,7 +14,7 @@ export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved
<div className="mb-1 ml-[130px] max-w-[200px]" style={{ lineHeight: 1 }}>
<a
className="text-xs !text-blue-600"
href="https://postgresus.com/notifier-slack"
href="https://postgresus.com/notifiers/slack"
target="_blank"
rel="noreferrer"
>

View File

@@ -30,7 +30,7 @@ export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved
<div className="mb-1 ml-[130px] max-w-[200px]" style={{ lineHeight: 1 }}>
<a
className="text-xs !text-blue-600"
href="https://postgresus.com/notifier-teams"
href="https://postgresus.com/notifiers/teams"
target="_blank"
rel="noreferrer"
>

View File

@@ -212,7 +212,7 @@ export function SettingsComponent({ contentHeight }: Props) {
<div className="mt-3 text-sm text-gray-500">
Read more about settings you can{' '}
<a
href="https://postgresus.com/settings"
href="https://postgresus.com/access-management/#global-settings"
target="_blank"
rel="noreferrer"
className="!text-blue-600"
@@ -230,23 +230,18 @@ export function SettingsComponent({ contentHeight }: Props) {
<code
className="flex-1 cursor-pointer transition-colors select-all hover:text-blue-600"
onClick={() => {
window.open(
`${getApplicationServer()}/api/v1/downdetect/is-available`,
'_blank',
);
window.open(`${getApplicationServer()}/api/v1/system/health`, '_blank');
}}
title="Click to open in new tab"
>
{getApplicationServer()}/api/v1/downdetect/is-available
{getApplicationServer()}/api/v1/system/health
</code>
<Button
type="text"
size="small"
className="ml-2 opacity-0 transition-opacity group-hover:opacity-100"
onClick={() => {
navigator.clipboard.writeText(
`${getApplicationServer()}/api/v1/downdetect/is-available`,
);
navigator.clipboard.writeText(`${getApplicationServer()}/api/v1/system/health`);
message.success('Health-check endpoint copied to clipboard');
}}
>

View File

@@ -40,7 +40,7 @@ export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsa
<div className="min-w-[110px]" />
<div className="text-xs text-blue-600">
<a href="https://postgresus.com/google-drive-storage" target="_blank" rel="noreferrer">
<a href="https://postgresus.com/storages/google-drive" target="_blank" rel="noreferrer">
How to connect Google Drive?
</a>
</div>

View File

@@ -16,7 +16,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
<div className="min-w-[110px]" />
<div className="text-xs text-blue-600">
<a href="https://postgresus.com/cloudflare-r2-storage" target="_blank" rel="noreferrer">
<a href="https://postgresus.com/storages/cloudflare-r2" target="_blank" rel="noreferrer">
How to use with Cloudflare R2?
</a>
</div>