mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
22
Dockerfile
22
Dockerfile
@@ -257,6 +257,9 @@ COPY backend/migrations ./migrations
|
||||
# Copy UI files
|
||||
COPY --from=backend-build /app/ui/build ./ui/build
|
||||
|
||||
# Copy cloud static HTML template (injected into index.html at startup when IS_CLOUD=true)
|
||||
COPY frontend/cloud-root-content.html /app/cloud-root-content.html
|
||||
|
||||
# Copy agent binaries (both architectures) — served by the backend
|
||||
# at GET /api/v1/system/agent?arch=amd64|arm64
|
||||
COPY --from=agent-build /agent-binaries ./agent-binaries
|
||||
@@ -326,6 +329,23 @@ if [ -n "\${ANALYTICS_SCRIPT:-}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Inject static HTML into root div for cloud mode (payment system requires visible legal links)
|
||||
if [ "\${IS_CLOUD:-false}" = "true" ]; then
|
||||
if ! grep -q "cloud-static-content" /app/ui/build/index.html 2>/dev/null; then
|
||||
echo "Injecting cloud static HTML content..."
|
||||
perl -i -pe '
|
||||
BEGIN {
|
||||
open my \$fh, "<", "/app/cloud-root-content.html" or die;
|
||||
local \$/;
|
||||
\$c = <\$fh>;
|
||||
close \$fh;
|
||||
\$c =~ s/\\n/ /g;
|
||||
}
|
||||
s/<div id="root"><\\/div>/<div id="root"><!-- cloud-static-content -->\$c<\\/div>/
|
||||
' /app/ui/build/index.html
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure proper ownership of data directory
|
||||
echo "Setting up data directory permissions..."
|
||||
mkdir -p /databasus-data/pgdata
|
||||
@@ -439,7 +459,7 @@ fi
|
||||
echo "Setting up database and user..."
|
||||
gosu postgres \$PG_BIN/psql -p 5437 -h localhost -d postgres << 'SQL'
|
||||
|
||||
# We use stub password, because internal DB is not exposed outside container
|
||||
-- We use stub password, because internal DB is not exposed outside container
|
||||
ALTER USER postgres WITH PASSWORD 'Q1234567';
|
||||
SELECT 'CREATE DATABASE databasus OWNER postgres'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'databasus')
|
||||
|
||||
@@ -109,6 +109,7 @@ func (uc *CreateMariadbBackupUsecase) buildMariadbDumpArgs(
|
||||
"--routines",
|
||||
"--quick",
|
||||
"--skip-extended-insert",
|
||||
"--skip-add-locks",
|
||||
"--verbose",
|
||||
}
|
||||
|
||||
@@ -129,6 +130,8 @@ func (uc *CreateMariadbBackupUsecase) buildMariadbDumpArgs(
|
||||
if mdb.IsHttps {
|
||||
args = append(args, "--ssl")
|
||||
args = append(args, "--skip-ssl-verify-server-cert")
|
||||
} else {
|
||||
args = append(args, "--skip-ssl")
|
||||
}
|
||||
|
||||
if mdb.Database != nil && *mdb.Database != "" {
|
||||
|
||||
@@ -108,6 +108,7 @@ func (uc *CreateMysqlBackupUsecase) buildMysqldumpArgs(my *mysqltypes.MysqlDatab
|
||||
"--set-gtid-purged=OFF",
|
||||
"--quick",
|
||||
"--skip-extended-insert",
|
||||
"--skip-add-locks",
|
||||
"--verbose",
|
||||
}
|
||||
|
||||
@@ -126,6 +127,8 @@ func (uc *CreateMysqlBackupUsecase) buildMysqldumpArgs(my *mysqltypes.MysqlDatab
|
||||
|
||||
if my.IsHttps {
|
||||
args = append(args, "--ssl-mode=REQUIRED")
|
||||
} else {
|
||||
args = append(args, "--ssl-mode=DISABLED")
|
||||
}
|
||||
|
||||
if my.Database != nil && *my.Database != "" {
|
||||
@@ -326,6 +329,8 @@ port=%d
|
||||
|
||||
if myConfig.IsHttps {
|
||||
content += "ssl-mode=REQUIRED\n"
|
||||
} else {
|
||||
content += "ssl-mode=DISABLED\n"
|
||||
}
|
||||
|
||||
err = os.WriteFile(myCnfFile, []byte(content), 0o600)
|
||||
|
||||
@@ -77,6 +77,8 @@ func (uc *RestoreMariadbBackupUsecase) Execute(
|
||||
if mdb.IsHttps {
|
||||
args = append(args, "--ssl")
|
||||
args = append(args, "--skip-ssl-verify-server-cert")
|
||||
} else {
|
||||
args = append(args, "--skip-ssl")
|
||||
}
|
||||
|
||||
if mdb.Database != nil && *mdb.Database != "" {
|
||||
|
||||
@@ -76,6 +76,8 @@ func (uc *RestoreMysqlBackupUsecase) Execute(
|
||||
|
||||
if my.IsHttps {
|
||||
args = append(args, "--ssl-mode=REQUIRED")
|
||||
} else {
|
||||
args = append(args, "--ssl-mode=DISABLED")
|
||||
}
|
||||
|
||||
if my.Database != nil && *my.Database != "" {
|
||||
@@ -305,6 +307,8 @@ port=%d
|
||||
|
||||
if myConfig.IsHttps {
|
||||
content += "ssl-mode=REQUIRED\n"
|
||||
} else {
|
||||
content += "ssl-mode=DISABLED\n"
|
||||
}
|
||||
|
||||
err = os.WriteFile(myCnfFile, []byte(content), 0o600)
|
||||
|
||||
40
frontend/cloud-root-content.html
Normal file
40
frontend/cloud-root-content.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<div style="font-family: 'Jost', system-ui, -apple-system, sans-serif; display: flex; flex-direction: column; min-height: 100vh; min-height: 100dvh; background: #fff;">
|
||||
<!-- Navbar -->
|
||||
<div style="display: flex; align-items: center; padding: 20px 20px 0; height: 65px;">
|
||||
<a href="https://databasus.com" target="_blank" rel="noreferrer" style="display: flex; align-items: center; gap: 12px; text-decoration: none;">
|
||||
<img src="/logo.svg" style="height: 45px; width: 45px; padding: 4px;" alt="Databasus" />
|
||||
<span style="font-size: 1.25rem; font-weight: 700; color: #2563eb;">Databasus</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Sign-in form stub -->
|
||||
<div style="display: flex; justify-content: center; margin-top: 10vh;">
|
||||
<div style="width: 100%; max-width: 300px; padding: 0 16px; box-sizing: border-box;">
|
||||
<div style="text-align: center; font-size: 1.5rem; font-weight: 700; margin-bottom: 24px; color: #111;">Sign in</div>
|
||||
|
||||
<div style="margin-bottom: 4px; font-size: 14px; font-weight: 600; color: #111;">Your email</div>
|
||||
<input type="email" placeholder="your@email.com" disabled
|
||||
style="width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box; font-size: 14px; font-family: inherit; background: #fff;" />
|
||||
|
||||
<div style="margin: 12px 0 4px; font-size: 14px; font-weight: 600; color: #111;">Password</div>
|
||||
<input type="password" placeholder="********" disabled
|
||||
style="width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box; font-size: 14px; font-family: inherit; background: #fff;" />
|
||||
|
||||
<button disabled
|
||||
style="width: 100%; margin-top: 16px; padding: 8px 16px; background: #1677ff; color: #fff; border: none; border-radius: 6px; font-size: 14px; font-family: inherit; cursor: default;">
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="margin-top: auto; padding-bottom: 20px; text-align: center;">
|
||||
<div style="font-size: 14px; color: #4b5563;">
|
||||
<a href="https://databasus.com/terms-of-use-cloud" style="color: #2563eb; text-decoration: underline;">Terms of Use</a>
|
||||
|
|
||||
<a href="https://databasus.com/privacy-cloud" style="color: #2563eb; text-decoration: underline;">Privacy Policy</a>
|
||||
</div>
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #6b7280;">
|
||||
info@databasus.com | © 2026 Databasus. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
export { PlaygroundWarningComponent } from './ui/PlaygroundWarningComponent';
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Modal } from 'antd';
|
||||
import type { JSX } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { IS_CLOUD } from '../../../constants';
|
||||
|
||||
const STORAGE_KEY = 'databasus_playground_info_dismissed';
|
||||
|
||||
const TIMEOUT_SECONDS = 30;
|
||||
|
||||
export const PlaygroundWarningComponent = (): JSX.Element => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(TIMEOUT_SECONDS);
|
||||
const [isButtonEnabled, setIsButtonEnabled] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
} catch (e) {
|
||||
console.warn('Failed to save playground modal state to localStorage:', e);
|
||||
}
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_CLOUD) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isDismissed = localStorage.getItem(STORAGE_KEY) === 'true';
|
||||
if (!isDismissed) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to read playground modal state from localStorage:', e);
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemainingSeconds((prev) => {
|
||||
if (prev <= 1) {
|
||||
setIsButtonEnabled(true);
|
||||
clearInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Welcome to Databasus playground"
|
||||
open={isVisible}
|
||||
onOk={handleClose}
|
||||
okText={
|
||||
<div className="min-w-[100px]">
|
||||
{isButtonEnabled ? 'Understood' : `${remainingSeconds}`}
|
||||
</div>
|
||||
}
|
||||
okButtonProps={{ disabled: !isButtonEnabled }}
|
||||
closable={false}
|
||||
cancelButtonProps={{ style: { display: 'none' } }}
|
||||
width={500}
|
||||
centered
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className="space-y-6 py-4">
|
||||
<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 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>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-semibold">What is limit?</h3>
|
||||
<ul className="list-disc space-y-1 pl-5 text-gray-700 dark:text-gray-300">
|
||||
<li>Single backup size - 100 MB (~1.5 GB database)</li>
|
||||
<li>Store period - 7 days</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-semibold">Is it secure?</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Yes, it's regular Databasus installation, secured and maintained by Databasus team.
|
||||
More about security{' '}
|
||||
<a
|
||||
href="https://databasus.com/security"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
you can read here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-semibold">Can my data be currepted?</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
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. At once we may clean backups or something like this. At least, check
|
||||
your backups here once a week
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 text-lg font-semibold">What if I see an issue?</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Create{' '}
|
||||
<a
|
||||
href="https://github.com/databasus/databasus/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
GitHub issue
|
||||
</a>{' '}
|
||||
or write{' '}
|
||||
<a
|
||||
href="https://t.me/databasus_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
to the community
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -166,6 +166,8 @@ export function SignInComponent({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
|
||||
import { App, Button, Input } from 'antd';
|
||||
import { App, Button, Checkbox, Input } from 'antd';
|
||||
import { type JSX, useState } from 'react';
|
||||
|
||||
import { useCloudflareTurnstile } from '../../../shared/hooks/useCloudflareTurnstile';
|
||||
|
||||
import { GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID } from '../../../constants';
|
||||
import { GITHUB_CLIENT_ID, GOOGLE_CLIENT_ID, IS_CLOUD } from '../../../constants';
|
||||
import { userApi } from '../../../entity/users';
|
||||
import { StringUtils } from '../../../shared/lib';
|
||||
import { FormValidator } from '../../../shared/lib/FormValidator';
|
||||
@@ -34,6 +34,9 @@ export function SignUpComponent({ onSwitchToSignIn }: SignUpComponentProps): JSX
|
||||
|
||||
const [signUpError, setSignUpError] = useState('');
|
||||
|
||||
const [isTermsAccepted, setTermsAccepted] = useState(false);
|
||||
const [isPolicyAccepted, setPolicyAccepted] = useState(false);
|
||||
|
||||
const { token, containerRef, resetCloudflareTurnstile } = useCloudflareTurnstile();
|
||||
|
||||
const validateFieldsForSignUp = (): boolean => {
|
||||
@@ -179,10 +182,49 @@ export function SignUpComponent({ onSwitchToSignIn }: SignUpComponentProps): JSX
|
||||
|
||||
<div className="mt-3" />
|
||||
|
||||
{IS_CLOUD && (
|
||||
<div className="mb-3 space-y-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={isTermsAccepted}
|
||||
onChange={(e) => setTermsAccepted(e.target.checked)}
|
||||
>
|
||||
I agree to{' '}
|
||||
<a
|
||||
href="https://databasus.com/terms-of-use-cloud"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
style={{ color: 'inherit', textDecoration: 'underline' }}
|
||||
>
|
||||
Terms of Use
|
||||
</a>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={isPolicyAccepted}
|
||||
onChange={(e) => setPolicyAccepted(e.target.checked)}
|
||||
>
|
||||
I agree to{' '}
|
||||
<a
|
||||
href="https://databasus.com/privacy-cloud"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
style={{ color: 'inherit', textDecoration: 'underline' }}
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CloudflareTurnstileWidget containerRef={containerRef} />
|
||||
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || (IS_CLOUD && (!isTermsAccepted || !isPolicyAccepted))}
|
||||
loading={isLoading}
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
@@ -211,6 +253,8 @@ export function SignUpComponent({ onSwitchToSignIn }: SignUpComponentProps): JSX
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { IS_CLOUD } from '../constants';
|
||||
import { userApi } from '../entity/users';
|
||||
import { PlaygroundWarningComponent } from '../features/playground';
|
||||
import {
|
||||
AdminPasswordComponent,
|
||||
AuthNavbarComponent,
|
||||
@@ -42,7 +42,7 @@ export function AuthPageComponent() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full dark:bg-gray-900" style={{ height: screenHeight }}>
|
||||
<div className="flex min-h-full flex-col dark:bg-gray-900" style={{ minHeight: screenHeight }}>
|
||||
{isLoading ? (
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<Spin indicator={<LoadingOutlined spin />} size="large" />
|
||||
@@ -84,7 +84,31 @@ export function AuthPageComponent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PlaygroundWarningComponent />
|
||||
{IS_CLOUD && (
|
||||
<footer className="mx-10 mt-auto pb-5 text-center text-sm text-gray-500 dark:text-gray-500">
|
||||
<a
|
||||
href="https://databasus.com/terms-of-use-cloud"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
Terms of Use
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://databasus.com/privacy-cloud"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
{' | '}
|
||||
info@databasus.com | © 2026 Databasus. All rights reserved.
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { type WorkspaceResponse, workspaceApi } from '../../entity/workspaces';
|
||||
import { DatabasesComponent } from '../../features/databases/ui/DatabasesComponent';
|
||||
import { NotifiersComponent } from '../../features/notifiers/ui/NotifiersComponent';
|
||||
import { PlaygroundWarningComponent } from '../../features/playground';
|
||||
import { SettingsComponent } from '../../features/settings';
|
||||
import { StoragesComponent } from '../../features/storages/ui/StoragesComponent';
|
||||
import { ProfileComponent } from '../../features/users';
|
||||
@@ -381,8 +380,6 @@ export const MainScreenComponent = () => {
|
||||
workspacesCount={workspaces.length}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PlaygroundWarningComponent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user