Compare commits

...

13 Commits

Author SHA1 Message Date
Rostislav Dugin
6d0ae32d0c Merge pull request #240 from databasus/develop
FIX (oauth): Enable GitHub and Google OAuth
2026-01-10 20:15:43 +03:00
Rostislav Dugin
011985d723 FIX (oauth): Enable GitHub and Google OAuth 2026-01-10 19:19:37 +03:00
Rostislav Dugin
d677ee61de Merge pull request #239 from databasus/develop
FIX (mariadb): --skip-ssl-verify-server-cert for mariadb / mysql
2026-01-10 18:34:58 +03:00
Rostislav Dugin
c6b8f6e87a Merge pull request #237 from wzzrd/bugfix/disable_mariadb_mysql_ssl_verify
--skip-ssl-verify-server-cert for mariadb
2026-01-10 18:33:45 +03:00
Maxim Burgerhout
2bb5f93d00 --skip-ssl-verify-server-cert for mariadb / mysql
This change adds the --skip-ssl-verify-server-cert flag to mariadb
database connections for both backups and restores. This errors when
trying to verify certificates during those procedures.
2026-01-10 15:50:09 +01:00
Rostislav Dugin
b91c150300 Merge pull request #236 from databasus/develop
Develop
2026-01-10 15:19:19 +03:00
Rostislav Dugin
12b119ce40 FIX (readme): Update readme 2026-01-10 15:16:25 +03:00
Rostislav Dugin
7c6f0ab4ba FIX (mysql\mariadb): Use custom TLS handler to skip verification instead of build-in 2026-01-10 15:13:47 +03:00
Rostislav Dugin
6d2db4b298 Merge pull request #232 from databasus/develop
Develop
2026-01-09 11:12:27 +03:00
Rostislav Dugin
6397423298 FIX (temp folder): Ensure permissions 0700 for temp folders to meet PG requirements for .pgpass ownership 2026-01-09 11:10:50 +03:00
Rostislav Dugin
3470aae8e3 FIX (mysql\mariadb): Remove PROCESS permission check before backup, because it is not mandatory for backup 2026-01-09 11:02:14 +03:00
Rostislav Dugin
184fbcdb2c Merge pull request #230 from databasus/develop
FIX (temp): Use Databasus temp folder instead of system over PG backup
2026-01-08 20:41:45 +03:00
Rostislav Dugin
2d897dd722 FIX (temp): Use Databasus temp folder instead of system over PG backup 2026-01-08 20:40:22 +03:00
16 changed files with 222 additions and 125 deletions

View File

@@ -245,7 +245,10 @@ PG_BIN="/usr/lib/postgresql/17/bin"
# Ensure proper ownership of data directory
echo "Setting up data directory permissions..."
mkdir -p /databasus-data/pgdata
mkdir -p /databasus-data/temp
mkdir -p /databasus-data/backups
chown -R postgres:postgres /databasus-data
chmod 700 /databasus-data/temp
# Initialize PostgreSQL if not already initialized
if [ ! -s "/databasus-data/pgdata/PG_VERSION" ]; then

View File

@@ -2,7 +2,7 @@
<img src="assets/logo.svg" alt="Databasus Logo" width="250"/>
<h3>Backup tool for PostgreSQL, MySQL and MongoDB</h3>
<p>Databasus is a free, open source and self-hosted tool to backup databases. Make backups with different storages (S3, Google Drive, FTP, etc.) and notifications about progress (Slack, Discord, Telegram, etc.). Previously known as Postgresus (see migration guide).</p>
<p>Databasus is a free, open source and self-hosted tool to backup databases (with focus on PostgreSQL). Make backups with different storages (S3, Google Drive, FTP, etc.) and notifications about progress (Slack, Discord, Telegram, etc.). Previously known as Postgresus (see migration guide).</p>
<!-- Badges -->
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)

View File

@@ -122,6 +122,7 @@ func (uc *CreateMariadbBackupUsecase) buildMariadbDumpArgs(
if mdb.IsHttps {
args = append(args, "--ssl")
args = append(args, "--skip-ssl-verify-server-cert")
}
if mdb.Database != nil && *mdb.Database != "" {
@@ -265,11 +266,24 @@ func (uc *CreateMariadbBackupUsecase) createTempMyCnfFile(
mdbConfig *mariadbtypes.MariadbDatabase,
password string,
) (string, error) {
tempDir, err := os.MkdirTemp(config.GetEnv().TempFolder, "mycnf_"+uuid.New().String())
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "mycnf_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temp directory permissions: %w", err)
}
myCnfFile := filepath.Join(tempDir, ".my.cnf")
content := fmt.Sprintf(`[client]
@@ -287,6 +301,7 @@ port=%d
err = os.WriteFile(myCnfFile, []byte(content), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write .my.cnf: %w", err)
}

View File

@@ -280,11 +280,24 @@ func (uc *CreateMysqlBackupUsecase) createTempMyCnfFile(
myConfig *mysqltypes.MysqlDatabase,
password string,
) (string, error) {
tempDir, err := os.MkdirTemp(config.GetEnv().TempFolder, "mycnf_"+uuid.New().String())
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "mycnf_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temp directory permissions: %w", err)
}
myCnfFile := filepath.Join(tempDir, ".my.cnf")
content := fmt.Sprintf(`[client]
@@ -300,6 +313,7 @@ port=%d
err = os.WriteFile(myCnfFile, []byte(content), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write .my.cnf: %w", err)
}

View File

@@ -757,14 +757,28 @@ func (uc *CreatePostgresqlBackupUsecase) createTempPgpassFile(
escapedPassword,
)
tempDir, err := os.MkdirTemp("", "pgpass")
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "pgpass_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temporary directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temporary directory permissions: %w", err)
}
pgpassFile := filepath.Join(tempDir, ".pgpass")
err = os.WriteFile(pgpassFile, []byte(pgpassContent), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write temporary .pgpass file: %w", err)
}

View File

@@ -2,6 +2,7 @@ package mariadb
import (
"context"
"crypto/tls"
"database/sql"
"errors"
"fmt"
@@ -14,7 +15,7 @@ import (
"databasus-backend/internal/util/encryption"
"databasus-backend/internal/util/tools"
_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid"
)
@@ -398,8 +399,16 @@ func HasPrivilege(privileges, priv string) bool {
func (m *MariadbDatabase) buildDSN(password string, database string) string {
tlsConfig := "false"
if m.IsHttps {
tlsConfig = "skip-verify"
err := mysql.RegisterTLSConfig("mariadb-skip-verify", &tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
// Config might already be registered, which is fine
_ = err
}
tlsConfig = "mariadb-skip-verify"
}
return fmt.Sprintf(
@@ -562,9 +571,9 @@ func detectPrivileges(ctx context.Context, db *sql.DB, database string) (string,
}
// checkBackupPermissions verifies the user has sufficient privileges for mariadb-dump backup.
// Required: SELECT, SHOW VIEW, PROCESS. Optional: LOCK TABLES, TRIGGER, EVENT.
// Required: SELECT, SHOW VIEW
func checkBackupPermissions(privileges string) error {
requiredPrivileges := []string{"SELECT", "SHOW VIEW", "PROCESS"}
requiredPrivileges := []string{"SELECT", "SHOW VIEW"}
var missingPrivileges []string
for _, priv := range requiredPrivileges {
@@ -575,7 +584,7 @@ func checkBackupPermissions(privileges string) error {
if len(missingPrivileges) > 0 {
return fmt.Errorf(
"insufficient permissions for backup. Missing: %s. Required: SELECT, SHOW VIEW, PROCESS",
"insufficient permissions for backup. Missing: %s. Required: SELECT, SHOW VIEW",
strings.Join(missingPrivileges, ", "),
)
}

View File

@@ -2,6 +2,7 @@ package mysql
import (
"context"
"crypto/tls"
"database/sql"
"errors"
"fmt"
@@ -14,7 +15,7 @@ import (
"databasus-backend/internal/util/encryption"
"databasus-backend/internal/util/tools"
_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid"
)
@@ -399,8 +400,17 @@ func HasPrivilege(privileges, priv string) bool {
func (m *MysqlDatabase) buildDSN(password string, database string) string {
tlsConfig := "false"
if m.IsHttps {
tlsConfig = "skip-verify"
err := mysql.RegisterTLSConfig("mysql-skip-verify", &tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
// Config might already be registered, which is fine
_ = err
}
tlsConfig = "mysql-skip-verify"
}
return fmt.Sprintf(
@@ -532,9 +542,9 @@ func detectPrivileges(ctx context.Context, db *sql.DB, database string) (string,
}
// checkBackupPermissions verifies the user has sufficient privileges for mysqldump backup.
// Required: SELECT, SHOW VIEW, PROCESS. Optional: LOCK TABLES, TRIGGER, EVENT.
// Required: SELECT, SHOW VIEW
func checkBackupPermissions(privileges string) error {
requiredPrivileges := []string{"SELECT", "SHOW VIEW", "PROCESS"}
requiredPrivileges := []string{"SELECT", "SHOW VIEW"}
var missingPrivileges []string
for _, priv := range requiredPrivileges {
@@ -545,7 +555,7 @@ func checkBackupPermissions(privileges string) error {
if len(missingPrivileges) > 0 {
return fmt.Errorf(
"insufficient permissions for backup. Missing: %s. Required: SELECT, SHOW VIEW, PROCESS",
"insufficient permissions for backup. Missing: %s. Required: SELECT, SHOW VIEW",
strings.Join(missingPrivileges, ", "),
)
}

View File

@@ -71,6 +71,7 @@ func (uc *RestoreMariadbBackupUsecase) Execute(
if mdb.IsHttps {
args = append(args, "--ssl")
args = append(args, "--skip-ssl-verify-server-cert")
}
if mdb.Database != nil && *mdb.Database != "" {
@@ -265,11 +266,24 @@ func (uc *RestoreMariadbBackupUsecase) createTempMyCnfFile(
mdbConfig *mariadbtypes.MariadbDatabase,
password string,
) (string, error) {
tempDir, err := os.MkdirTemp(config.GetEnv().TempFolder, "mycnf_"+uuid.New().String())
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "mycnf_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temp directory permissions: %w", err)
}
myCnfFile := filepath.Join(tempDir, ".my.cnf")
content := fmt.Sprintf(`[client]
@@ -287,6 +301,7 @@ port=%d
err = os.WriteFile(myCnfFile, []byte(content), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write .my.cnf: %w", err)
}

View File

@@ -257,11 +257,24 @@ func (uc *RestoreMysqlBackupUsecase) createTempMyCnfFile(
myConfig *mysqltypes.MysqlDatabase,
password string,
) (string, error) {
tempDir, err := os.MkdirTemp(config.GetEnv().TempFolder, "mycnf_"+uuid.New().String())
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "mycnf_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temp directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temp directory permissions: %w", err)
}
myCnfFile := filepath.Join(tempDir, ".my.cnf")
content := fmt.Sprintf(`[client]
@@ -277,6 +290,7 @@ port=%d
err = os.WriteFile(myCnfFile, []byte(content), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write .my.cnf: %w", err)
}

View File

@@ -925,14 +925,28 @@ func (uc *RestorePostgresqlBackupUsecase) createTempPgpassFile(
escapedPassword,
)
tempDir, err := os.MkdirTemp(config.GetEnv().TempFolder, "pgpass_"+uuid.New().String())
tempFolder := config.GetEnv().TempFolder
if err := os.MkdirAll(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to ensure temp folder exists: %w", err)
}
if err := os.Chmod(tempFolder, 0700); err != nil {
return "", fmt.Errorf("failed to set temp folder permissions: %w", err)
}
tempDir, err := os.MkdirTemp(tempFolder, "pgpass_"+uuid.New().String())
if err != nil {
return "", fmt.Errorf("failed to create temporary directory: %w", err)
}
if err := os.Chmod(tempDir, 0700); err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to set temporary directory permissions: %w", err)
}
pgpassFile := filepath.Join(tempDir, ".pgpass")
err = os.WriteFile(pgpassFile, []byte(pgpassContent), 0600)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", fmt.Errorf("failed to write temporary .pgpass file: %w", err)
}

View File

@@ -40,7 +40,3 @@ export const GOOGLE_CLIENT_ID =
export function getOAuthRedirectUri(): string {
return `${window.location.origin}/auth/callback`;
}
export function isOAuthEnabled(): boolean {
return IS_CLOUD && (!!GITHUB_CLIENT_ID || !!GOOGLE_CLIENT_ID);
}

View File

@@ -1,96 +0,0 @@
import { GithubOutlined, GoogleOutlined } from '@ant-design/icons';
import { Button, message } from 'antd';
import {
GITHUB_CLIENT_ID,
GOOGLE_CLIENT_ID,
getOAuthRedirectUri,
isOAuthEnabled,
} from '../../../constants';
export function OauthComponent() {
if (!isOAuthEnabled()) {
return null;
}
const redirectUri = getOAuthRedirectUri();
const handleGitHubLogin = () => {
if (!GITHUB_CLIENT_ID) {
message.error('GitHub OAuth is not configured');
return;
}
try {
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: redirectUri,
state: 'github',
scope: 'user:email',
});
const githubAuthUrl = `https://github.com/login/oauth/authorize?${params.toString()}`;
// Validate URL is properly formed
new URL(githubAuthUrl);
window.location.href = githubAuthUrl;
} catch (error) {
message.error('Invalid OAuth configuration');
console.error('GitHub OAuth URL error:', error);
}
};
const handleGoogleLogin = () => {
if (!GOOGLE_CLIENT_ID) {
message.error('Google OAuth is not configured');
return;
}
try {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
state: 'google',
});
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
// Validate URL is properly formed
new URL(googleAuthUrl);
window.location.href = googleAuthUrl;
} catch (error) {
message.error('Invalid OAuth configuration');
console.error('Google OAuth URL error:', error);
}
};
return (
<div className="mt-4">
<div className="space-y-2">
{GITHUB_CLIENT_ID && (
<Button
icon={<GithubOutlined />}
onClick={handleGitHubLogin}
className="w-full"
size="large"
>
Continue with GitHub
</Button>
)}
{GOOGLE_CLIENT_ID && (
<Button
icon={<GoogleOutlined />}
onClick={handleGoogleLogin}
className="w-full"
size="large"
>
Continue with Google
</Button>
)}
</div>
</div>
);
}

View File

@@ -2,11 +2,12 @@ import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
import { Button, Input } from 'antd';
import { type JSX, useState } from 'react';
import { IS_CLOUD } from '../../../constants';
import { GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID } from '../../../constants';
import { userApi } from '../../../entity/users';
import { StringUtils } from '../../../shared/lib';
import { FormValidator } from '../../../shared/lib/FormValidator';
import { OauthComponent } from './OauthComponent';
import { GithubOAuthComponent } from './oauth/GithubOAuthComponent';
import { GoogleOAuthComponent } from './oauth/GoogleOAuthComponent';
interface SignInComponentProps {
onSwitchToSignUp?: () => void;
@@ -67,9 +68,14 @@ export function SignInComponent({ onSwitchToSignUp }: SignInComponentProps): JSX
<div className="w-full max-w-[300px]">
<div className="mb-5 text-center text-2xl font-bold">Sign in</div>
<OauthComponent />
<div className="mt-4">
<div className="space-y-2">
<GithubOAuthComponent />
<GoogleOAuthComponent />
</div>
</div>
{IS_CLOUD && (
{(GOOGLE_CLIENT_ID || GITHUB_CLIENT_ID) && (
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>

View File

@@ -2,11 +2,12 @@ import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
import { App, Button, Input } from 'antd';
import { type JSX, useState } from 'react';
import { IS_CLOUD } from '../../../constants';
import { GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID } from '../../../constants';
import { userApi } from '../../../entity/users';
import { StringUtils } from '../../../shared/lib';
import { FormValidator } from '../../../shared/lib/FormValidator';
import { OauthComponent } from './OauthComponent';
import { GithubOAuthComponent } from './oauth/GithubOAuthComponent';
import { GoogleOAuthComponent } from './oauth/GoogleOAuthComponent';
interface SignUpComponentProps {
onSwitchToSignIn?: () => void;
@@ -98,9 +99,14 @@ export function SignUpComponent({ onSwitchToSignIn }: SignUpComponentProps): JSX
<div className="w-full max-w-[300px]">
<div className="mb-5 text-center text-2xl font-bold">Sign up</div>
<OauthComponent />
<div className="mt-4">
<div className="space-y-2">
<GithubOAuthComponent />
<GoogleOAuthComponent />
</div>
</div>
{IS_CLOUD && (
{(GOOGLE_CLIENT_ID || GITHUB_CLIENT_ID) && (
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>

View File

@@ -0,0 +1,38 @@
import { GithubOutlined } from '@ant-design/icons';
import { Button, message } from 'antd';
import { GITHUB_CLIENT_ID, getOAuthRedirectUri } from '../../../../constants';
export function GithubOAuthComponent() {
if (!GITHUB_CLIENT_ID) {
return null;
}
const redirectUri = getOAuthRedirectUri();
const handleGitHubLogin = () => {
try {
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: redirectUri,
state: 'github',
scope: 'user:email',
});
const githubAuthUrl = `https://github.com/login/oauth/authorize?${params.toString()}`;
// Validate URL is properly formed
new URL(githubAuthUrl);
window.location.href = githubAuthUrl;
} catch (error) {
message.error('Invalid OAuth configuration');
console.error('GitHub OAuth URL error:', error);
}
};
return (
<Button icon={<GithubOutlined />} onClick={handleGitHubLogin} className="w-full" size="large">
Continue with GitHub
</Button>
);
}

View File

@@ -0,0 +1,39 @@
import { GoogleOutlined } from '@ant-design/icons';
import { Button, message } from 'antd';
import { GOOGLE_CLIENT_ID, getOAuthRedirectUri } from '../../../../constants';
export function GoogleOAuthComponent() {
if (!GOOGLE_CLIENT_ID) {
return null;
}
const redirectUri = getOAuthRedirectUri();
const handleGoogleLogin = () => {
try {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
state: 'google',
});
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
// Validate URL is properly formed
new URL(googleAuthUrl);
window.location.href = googleAuthUrl;
} catch (error) {
message.error('Invalid OAuth configuration');
console.error('Google OAuth URL error:', error);
}
};
return (
<Button icon={<GoogleOutlined />} onClick={handleGoogleLogin} className="w-full" size="large">
Continue with Google
</Button>
);
}