mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8eea05dca | ||
|
|
b2a94274d7 | ||
|
|
77c2712ebb | ||
|
|
a9dc29f82c | ||
|
|
c934a45dca | ||
|
|
d4acdf2826 | ||
|
|
49753c4fc0 | ||
|
|
c6aed6b36d | ||
|
|
3060b4266a | ||
|
|
ebeb597f17 | ||
|
|
4783784325 | ||
|
|
bd41433bdb | ||
|
|
a9073787d2 | ||
|
|
0890bf8f09 | ||
|
|
f8c11e8802 | ||
|
|
e798d82fc1 | ||
|
|
81a01585ee | ||
|
|
a8465c1a10 | ||
|
|
a9e5db70f6 | ||
|
|
7a47be6ca6 | ||
|
|
16be3db0c6 | ||
|
|
744e51d1e1 | ||
|
|
b3af75d430 | ||
|
|
6f7320abeb | ||
|
|
a1655d35a6 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user