Compare commits

...

25 Commits

Author SHA1 Message Date
Rostislav Dugin
d8eea05dca Merge pull request #332 from databasus/develop
FIX (script): Fix script creation in playground head x2
2026-02-02 20:46:35 +03:00
Rostislav Dugin
b2a94274d7 FIX (script): Fix script creation in playground head x2 2026-02-02 20:44:52 +03:00
Rostislav Dugin
77c2712ebb Merge pull request #331 from databasus/develop
FIX (script): Fix script creation in playground head
2026-02-02 19:47:44 +03:00
Rostislav Dugin
a9dc29f82c FIX (script): Fix script creation in playground head 2026-02-02 19:47:15 +03:00
Rostislav Dugin
c934a45dca Merge pull request #330 from databasus/develop
FIX (storages): Fix storage edit in playground
2026-02-02 18:51:47 +03:00
Rostislav Dugin
d4acdf2826 FIX (storages): Fix storage edit in playground 2026-02-02 18:48:19 +03:00
Rostislav Dugin
49753c4fc0 Merge pull request #329 from databasus/develop
FIX (s3): Fix S3 prefill in playground on form edit
2026-02-02 18:14:07 +03:00
Rostislav Dugin
c6aed6b36d FIX (s3): Fix S3 prefill in playground on form edit 2026-02-02 18:12:44 +03:00
Rostislav Dugin
3060b4266a Merge pull request #328 from databasus/develop
Develop
2026-02-02 17:53:05 +03:00
Rostislav Dugin
ebeb597f17 FEATURE (playground): Add support of Rybbit script for playground 2026-02-02 17:50:31 +03:00
Rostislav Dugin
4783784325 FIX (playground): Do not show whitelist message in playground 2026-02-02 16:53:01 +03:00
Rostislav Dugin
bd41433bdb Merge branch 'develop' of https://github.com/databasus/databasus into develop 2026-02-02 16:50:18 +03:00
Rostislav Dugin
a9073787d2 FIX (audit logs): In dark mode show white text in audit logs 2026-02-02 16:44:49 +03:00
Rostislav Dugin
0890bf8f09 Merge pull request #327 from artemkalugin01/access-management-href-fix
Fix href in settings for access-management#global-settings
2026-02-02 16:12:25 +03:00
artem.kalugin
f8c11e8802 Fix href typo in settings for access-management#global-settings 2026-02-02 12:59:56 +03:00
Rostislav Dugin
e798d82fc1 Merge pull request #325 from databasus/develop
FIX (storages): Fix default storage type prefill in playground
2026-02-01 20:12:12 +03:00
Rostislav Dugin
81a01585ee FIX (storages): Fix default storage type prefill in playground 2026-02-01 20:07:12 +03:00
Rostislav Dugin
a8465c1a10 Merge pull request #324 from databasus/develop
FIX (storages): Limit local storage usage in playground
2026-02-01 19:20:34 +03:00
Rostislav Dugin
a9e5db70f6 FIX (storages): Limit local storage usage in playground 2026-02-01 19:18:54 +03:00
Rostislav Dugin
7a47be6ca6 Merge pull request #323 from databasus/develop
Develop
2026-02-01 18:42:30 +03:00
Rostislav Dugin
16be3db0c6 FIX (playground): Pre-select system storage if exists in playground 2026-02-01 18:30:50 +03:00
Rostislav Dugin
744e51d1e1 REFACTOR (email): Refactor commit adding date headers to emails 2026-02-01 16:43:53 +03:00
Rostislav Dugin
b3af75d430 Merge branch 'develop' of https://github.com/databasus/databasus into develop 2026-02-01 16:41:52 +03:00
mcarbs
6f7320abeb FIX (email): Add email date header 2026-02-01 16:41:17 +03:00
Rostislav Dugin
a1655d35a6 FIX (healthcheck): Add cache accessibility to healthcheck 2026-01-30 16:33:39 +03:00
20 changed files with 144 additions and 62 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,4 @@
ansible/
postgresus_data/
postgresus-data/
databasus-data/
@@ -9,4 +10,5 @@ node_modules/
/articles
.DS_Store
/scripts
/scripts
.vscode/settings.json

View File

@@ -272,6 +272,15 @@ window.__RUNTIME_CONFIG__ = {
};
JSEOF
# Inject analytics script if provided (only if not already injected)
if [ -n "\${ANALYTICS_SCRIPT:-}" ]; then
if ! grep -q "rybbit.databasus.com" /app/ui/build/index.html 2>/dev/null; then
echo "Injecting analytics script..."
sed -i "s#</head># \${ANALYTICS_SCRIPT}\\
</head>#" /app/ui/build/index.html
fi
fi
# Ensure proper ownership of data directory
echo "Setting up data directory permissions..."
mkdir -p /databasus-data/pgdata

View File

@@ -52,6 +52,7 @@ func (s *EmailSMTPSender) buildEmailContent(to, subject, body, from string) []by
// Encode Subject header using RFC 2047 to avoid SMTPUTF8 requirement
encodedSubject := encodeRFC2047(subject)
subjectHeader := fmt.Sprintf("Subject: %s\r\n", encodedSubject)
dateHeader := fmt.Sprintf("Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
mimeHeaders := fmt.Sprintf(
"MIME-version: 1.0;\nContent-Type: %s; charset=\"%s\";\n\n",
@@ -65,7 +66,7 @@ func (s *EmailSMTPSender) buildEmailContent(to, subject, body, from string) []by
toHeader := fmt.Sprintf("To: %s\r\n", to)
return []byte(fromHeader + toHeader + subjectHeader + mimeHeaders + body)
return []byte(fromHeader + toHeader + subjectHeader + dateHeader + mimeHeaders + body)
}
func (s *EmailSMTPSender) sendImplicitTLS(

View File

@@ -130,6 +130,7 @@ func (e *EmailNotifier) buildEmailContent(heading, message, from string) []byte
// This ensures compatibility with SMTP servers that don't support SMTPUTF8
encodedSubject := encodeRFC2047(heading)
subject := fmt.Sprintf("Subject: %s\r\n", encodedSubject)
dateHeader := fmt.Sprintf("Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
mimeHeaders := fmt.Sprintf(
"MIME-version: 1.0;\nContent-Type: %s; charset=\"%s\";\n\n",
@@ -143,7 +144,7 @@ func (e *EmailNotifier) buildEmailContent(heading, message, from string) []byte
toHeader := fmt.Sprintf("To: %s\r\n", e.TargetEmail)
return []byte(fromHeader + toHeader + subject + mimeHeaders + message)
return []byte(fromHeader + toHeader + subject + dateHeader + mimeHeaders + message)
}
func (e *EmailNotifier) sendImplicitTLS(

View File

@@ -58,7 +58,8 @@ func (c *StorageController) SaveStorage(ctx *gin.Context) {
}
if err := c.storageService.SaveStorage(user, request.WorkspaceID, &request); err != nil {
if errors.Is(err, ErrInsufficientPermissionsToManageStorage) {
if errors.Is(err, ErrInsufficientPermissionsToManageStorage) ||
errors.Is(err, ErrLocalStorageNotAllowedInCloudMode) {
ctx.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
@@ -325,7 +326,11 @@ func (c *StorageController) TestStorageConnectionDirect(ctx *gin.Context) {
return
}
if err := c.storageService.TestStorageConnectionDirect(&request); err != nil {
if err := c.storageService.TestStorageConnectionDirect(user, &request); err != nil {
if errors.Is(err, ErrLocalStorageNotAllowedInCloudMode) {
ctx.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

View File

@@ -39,4 +39,7 @@ var (
ErrSystemStorageCannotBeMadePrivate = errors.New(
"system storage cannot be changed to non-system",
)
ErrLocalStorageNotAllowedInCloudMode = errors.New(
"local storage can only be managed by administrators in cloud mode",
)
)

View File

@@ -3,6 +3,7 @@ package storages
import (
"fmt"
"databasus-backend/internal/config"
audit_logs "databasus-backend/internal/features/audit_logs"
users_enums "databasus-backend/internal/features/users/enums"
users_models "databasus-backend/internal/features/users/models"
@@ -37,6 +38,11 @@ func (s *StorageService) SaveStorage(
return ErrInsufficientPermissionsToManageStorage
}
if config.GetEnv().IsCloud && storage.Type == StorageTypeLocal &&
user.Role != users_enums.UserRoleAdmin {
return ErrLocalStorageNotAllowedInCloudMode
}
isUpdate := storage.ID != uuid.Nil
if storage.IsSystem && user.Role != users_enums.UserRoleAdmin {
@@ -238,8 +244,14 @@ func (s *StorageService) TestStorageConnection(
}
func (s *StorageService) TestStorageConnectionDirect(
user *users_models.User,
storage *Storage,
) error {
if config.GetEnv().IsCloud && storage.Type == StorageTypeLocal &&
user.Role != users_enums.UserRoleAdmin {
return ErrLocalStorageNotAllowedInCloudMode
}
var usingStorage *Storage
if storage.ID != uuid.Nil {

View File

@@ -1,11 +1,14 @@
package system_healthcheck
import (
"context"
"databasus-backend/internal/config"
"databasus-backend/internal/features/backups/backups/backuping"
"databasus-backend/internal/features/disk"
"databasus-backend/internal/storage"
cache_utils "databasus-backend/internal/util/cache"
"errors"
"time"
)
type HealthcheckService struct {
@@ -15,6 +18,20 @@ type HealthcheckService struct {
}
func (s *HealthcheckService) IsHealthy() error {
return s.performHealthCheck()
}
func (s *HealthcheckService) performHealthCheck() error {
// Check if cache is available with PING
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
client := cache_utils.GetValkeyClient()
pingResult := client.Do(ctx, client.B().Ping().Build())
if pingResult.Error() != nil {
return errors.New("cannot connect to valkey")
}
diskUsage, err := s.diskService.GetDiskUsage()
if err != nil {
return errors.New("cannot get disk usage")
@@ -40,6 +57,7 @@ func (s *HealthcheckService) IsHealthy() error {
if config.GetEnv().IsProcessingNode {
if !s.backuperNode.IsBackuperRunning() {
return errors.New("backuper node is not running for more than 5 minutes")
}
}

View File

@@ -204,6 +204,13 @@ export const EditBackupConfigComponent = ({
try {
const storages = await storageApi.getStorages(database.workspaceId);
setStorages(storages);
if (IS_CLOUD) {
const systemStorages = storages.filter((s) => s.isSystem);
if (systemStorages.length > 0) {
updateBackupConfig({ storage: systemStorages[0] });
}
}
} catch (e) {
alert((e as Error).message);
}

View File

@@ -402,7 +402,7 @@ export const EditMariaDbSpecificDataComponent = ({
)}
</div>
{isConnectionFailed && (
{isConnectionFailed && !IS_CLOUD && (
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">
If your database uses IP whitelist, make sure Databasus server IP is added to the allowed
list.

View File

@@ -425,7 +425,7 @@ export const EditMongoDbSpecificDataComponent = ({
)}
</div>
{isConnectionFailed && (
{isConnectionFailed && !IS_CLOUD && (
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">
If your database uses IP whitelist, make sure Databasus server IP is added to the allowed
list.

View File

@@ -353,7 +353,7 @@ export const EditMySqlSpecificDataComponent = ({
)}
</div>
{isConnectionFailed && (
{isConnectionFailed && !IS_CLOUD && (
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">
If your database uses IP whitelist, make sure Databasus server IP is added to the allowed
list.

View File

@@ -514,7 +514,7 @@ export const EditPostgreSqlSpecificDataComponent = ({
)}
</div>
{isConnectionFailed && (
{isConnectionFailed && !IS_CLOUD && (
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">
If your database uses IP whitelist, make sure Databasus server IP is added to the allowed
list.

View File

@@ -59,7 +59,7 @@ export const PlaygroundWarningComponent = (): JSX.Element => {
return (
<Modal
title="Welcome to Databasus Playground"
title="Welcome to Databasus playground"
open={isVisible}
onOk={handleClose}
okText={
@@ -78,9 +78,10 @@ export const PlaygroundWarningComponent = (): JSX.Element => {
<div>
<h3 className="mb-2 text-lg font-semibold">What is Playground?</h3>
<p className="text-gray-700 dark:text-gray-300">
Playground is a dev environment where you can test small databases backup and see
Databasus in action. Databasus dev team can test new features and see issues which hard
to detect when using self hosted (without logs or reports)
Playground is a dev environment of Databasus development team. It is used by Databasus
dev team to test new features and see issues which hard to detect when using self hosted
(without logs or reports).{' '}
<b>Here you can make backups for small and not critical databases for free</b>
</p>
</div>
@@ -114,7 +115,8 @@ export const PlaygroundWarningComponent = (): JSX.Element => {
No, because playground use only read-only users and cannot affect your DB. Only issue
you can face is instability: playground background workers frequently reloaded so backup
can be slower or be restarted due to app restart. Do not rely production DBs on
playground, please
playground, please. At once we may clean backups or something like this. At least, check
your backups here once a week
</p>
</div>

View File

@@ -218,7 +218,7 @@ export function SettingsComponent({ contentHeight }: Props) {
<div className="mt-3 text-sm text-gray-500 dark:text-gray-400">
Read more about settings you can{' '}
<a
href="https://databasus.com/access-management/#global-settings"
href="https://databasus.com/access-management#global-settings"
target="_blank"
rel="noreferrer"
className="!text-blue-600"

View File

@@ -42,7 +42,7 @@ export const StorageCardComponent = ({
)}
{storage.isSystem && (
<div className="mt-2 inline-block rounded-lg bg-[#ffffff10] px-2 py-1 text-xs text-gray-700 dark:text-gray-300">
<div className="mt-2 inline-block rounded-xl bg-[#00000010] px-2 py-1 text-xs text-gray-700 dark:bg-[#ffffff10] dark:text-gray-300">
System storage
</div>
)}

View File

@@ -143,15 +143,22 @@ export const StorageComponent = ({
) : (
<div>
{!isEditName ? (
<div className="mb-5 flex items-center text-2xl font-bold">
{storage.name}
{(storage.isSystem && user.role === UserRole.ADMIN) ||
(isCanManageStorages && (
<>
<div className="mb-5 flex items-center text-2xl font-bold">
{storage.name}
{(!storage.isSystem || user.role === UserRole.ADMIN) && isCanManageStorages && (
<div className="ml-2 cursor-pointer" onClick={() => startEdit('name')}>
<img src="/icons/pen-gray.svg" />
</div>
))}
</div>
)}
</div>
{storage.isSystem && (
<span className="mt-2 inline-block rounded-xl bg-[#00000010] px-2 py-1 text-xs text-gray-700 dark:bg-[#ffffff10] dark:text-gray-300">
System storage
</span>
)}
</>
) : (
<div>
<div className="flex items-center">
@@ -220,19 +227,22 @@ export const StorageComponent = ({
</div>
)}
<div className="mt-5 flex items-center font-bold">
<div>Storage settings</div>
{(!storage.isSystem || user.role === UserRole.ADMIN) && (
<div className="mt-5 flex items-center font-bold">
<div>Storage settings</div>
{!isEditSettings &&
isCanManageStorages &&
!(storage.isSystem && user.role !== UserRole.ADMIN) ? (
<div className="ml-2 h-4 w-4 cursor-pointer" onClick={() => startEdit('settings')}>
<img src="/icons/pen-gray.svg" />
</div>
) : (
<div />
)}
</div>
{!isEditSettings && isCanManageStorages ? (
<div
className="ml-2 h-4 w-4 cursor-pointer"
onClick={() => startEdit('settings')}
>
<img src="/icons/pen-gray.svg" />
</div>
) : (
<div />
)}
</div>
)}
<div className="mt-1 text-sm">
{isEditSettings && isCanManageStorages ? (
@@ -254,7 +264,7 @@ export const StorageComponent = ({
)}
</div>
{!isEditSettings && (
{!isEditSettings && (!storage.isSystem || user.role === UserRole.ADMIN) && (
<div className="mt-5">
<Button
type="primary"

View File

@@ -193,9 +193,18 @@ export function EditStorageComponent({
id: undefined as unknown as string,
workspaceId,
name: '',
type: StorageType.LOCAL,
type: IS_CLOUD ? StorageType.S3 : StorageType.LOCAL,
isSystem: false,
localStorage: {},
localStorage: IS_CLOUD ? undefined : {},
s3Storage: IS_CLOUD
? {
s3Bucket: '',
s3Region: '',
s3AccessKey: '',
s3SecretKey: '',
s3Endpoint: '',
}
: undefined,
},
);
}, [editingStorage]);
@@ -317,6 +326,22 @@ export function EditStorageComponent({
if (!storage) return <div />;
const storageTypeOptions = [
{ label: 'Local storage', value: StorageType.LOCAL },
{ label: 'S3', value: StorageType.S3 },
{ label: 'Google Drive', value: StorageType.GOOGLE_DRIVE },
{ label: 'NAS', value: StorageType.NAS },
{ label: 'Azure Blob Storage', value: StorageType.AZURE_BLOB },
{ label: 'FTP', value: StorageType.FTP },
{ label: 'SFTP', value: StorageType.SFTP },
{ label: 'Rclone', value: StorageType.RCLONE },
].filter((option) => {
if (IS_CLOUD && option.value === StorageType.LOCAL && user.role !== UserRole.ADMIN) {
return false;
}
return true;
});
return (
<div>
{isShowName && (
@@ -342,16 +367,7 @@ export function EditStorageComponent({
<div className="flex items-center">
<Select
value={storage?.type}
options={[
{ label: 'Local storage', value: StorageType.LOCAL },
{ label: 'S3', value: StorageType.S3 },
{ label: 'Google Drive', value: StorageType.GOOGLE_DRIVE },
{ label: 'NAS', value: StorageType.NAS },
{ label: 'Azure Blob Storage', value: StorageType.AZURE_BLOB },
{ label: 'FTP', value: StorageType.FTP },
{ label: 'SFTP', value: StorageType.SFTP },
{ label: 'Rclone', value: StorageType.RCLONE },
]}
options={storageTypeOptions}
onChange={(value) => {
setStorageType(value);
setIsUnsaved(true);

View File

@@ -18,6 +18,8 @@ interface Props {
export function ShowStorageComponent({ storage, user }: Props) {
if (!storage) return null;
if (storage?.isSystem && user.role !== UserRole.ADMIN) return <div />;
return (
<div>
<div className="mb-1 flex items-center">
@@ -39,33 +41,23 @@ export function ShowStorageComponent({ storage, user }: Props) {
</div>
)}
<div>{storage?.type === StorageType.S3 && <ShowS3StorageComponent storage={storage} />}</div>
<div>
{storage?.type === StorageType.S3 && <ShowS3StorageComponent storage={storage} />}
{storage?.type === StorageType.GOOGLE_DRIVE && (
<ShowGoogleDriveStorageComponent storage={storage} />
)}
</div>
<div>
{storage?.type === StorageType.NAS && <ShowNASStorageComponent storage={storage} />}
</div>
<div>
{storage?.type === StorageType.AZURE_BLOB && (
<ShowAzureBlobStorageComponent storage={storage} />
)}
</div>
<div>
{storage?.type === StorageType.FTP && <ShowFTPStorageComponent storage={storage} />}
</div>
<div>
{storage?.type === StorageType.SFTP && <ShowSFTPStorageComponent storage={storage} />}
</div>
<div>
{storage?.type === StorageType.RCLONE && <ShowRcloneStorageComponent storage={storage} />}
</div>
</div>

View File

@@ -101,7 +101,9 @@ export function UserAuditLogsSidebarComponent({ user }: Props) {
dataIndex: 'message',
key: 'message',
width: 350,
render: (message: string) => <span className="text-xs text-gray-900">{message}</span>,
render: (message: string) => (
<span className="text-xs text-gray-900 dark:text-white">{message}</span>
),
},
{
title: 'Workspace',
@@ -111,7 +113,9 @@ export function UserAuditLogsSidebarComponent({ user }: Props) {
render: (workspaceId: string | undefined) => (
<span
className={`inline-block rounded-full px-2 py-1 text-xs font-medium ${
workspaceId ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600'
workspaceId
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-white'
}`}
>
{workspaceId || '-'}
@@ -127,7 +131,7 @@ export function UserAuditLogsSidebarComponent({ user }: Props) {
const date = dayjs(createdAt);
const timeFormat = getUserTimeFormat();
return (
<span className="text-xs text-gray-700">
<span className="text-xs text-gray-700 dark:text-white">
{`${date.format(timeFormat.format)} (${date.fromNow()})`}
</span>
);