diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py
index 53416b865a..888dad83cc 100644
--- a/api/controllers/console/error.py
+++ b/api/controllers/console/error.py
@@ -13,6 +13,16 @@ class NotSetupError(BaseHTTPException):
"Please proceed with the initialization and installation process first."
code = 401
+class NotInitValidateError(BaseHTTPException):
+ error_code = 'not_init_validated'
+ description = "Init validation has not been completed yet. " \
+ "Please proceed with the init validation process first."
+ code = 401
+
+class InitValidateFailedError(BaseHTTPException):
+ error_code = 'init_validate_failed'
+ description = "Init validation failed. Please check the password and try again."
+ code = 401
class AccountNotLinkTenantError(BaseHTTPException):
error_code = 'account_not_link_tenant'
diff --git a/api/controllers/console/init_validate.py b/api/controllers/console/init_validate.py
new file mode 100644
index 0000000000..ebe8b96d61
--- /dev/null
+++ b/api/controllers/console/init_validate.py
@@ -0,0 +1,47 @@
+import os
+from flask import current_app, session
+from flask_restful import Resource, reqparse
+from libs.helper import str_len
+from models.model import DifySetup
+from services.account_service import TenantService
+
+from . import api
+from .error import AlreadySetupError, InitValidateFailedError
+from .wraps import only_edition_self_hosted
+
+
+class InitValidateAPI(Resource):
+
+ def get(self):
+ init_status = get_init_validate_status()
+ if init_status:
+ return { 'status': 'finished' }
+ return {'status': 'not_started' }
+
+ @only_edition_self_hosted
+ def post(self):
+ # is tenant created
+ tenant_count = TenantService.get_tenant_count()
+ if tenant_count > 0:
+ raise AlreadySetupError()
+
+ parser = reqparse.RequestParser()
+ parser.add_argument('password', type=str_len(30),
+ required=True, location='json')
+ input_password = parser.parse_args()['password']
+
+ if input_password != os.environ.get('INIT_PASSWORD'):
+ session['is_init_validated'] = False
+ raise InitValidateFailedError()
+
+ session['is_init_validated'] = True
+ return {'result': 'success'}, 201
+
+def get_init_validate_status():
+ if current_app.config['EDITION'] == 'SELF_HOSTED':
+ if os.environ.get('INIT_PASSWORD'):
+ return session.get('is_init_validated') or DifySetup.query.first()
+
+ return True
+
+api.add_resource(InitValidateAPI, '/init')
diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py
index ad37561e42..040f9f23b7 100644
--- a/api/controllers/console/setup.py
+++ b/api/controllers/console/setup.py
@@ -10,7 +10,8 @@ from models.model import DifySetup
from services.account_service import AccountService, RegisterService, TenantService
from . import api
-from .error import AlreadySetupError, NotSetupError
+from .error import AlreadySetupError, NotSetupError, NotInitValidateError
+from .init_validate import get_init_validate_status
from .wraps import only_edition_self_hosted
@@ -24,7 +25,7 @@ class SetupApi(Resource):
'step': 'finished',
'setup_at': setup_status.setup_at.isoformat()
}
- return {'step': 'not_start'}
+ return {'step': 'not_started'}
return {'step': 'finished'}
@only_edition_self_hosted
@@ -37,6 +38,9 @@ class SetupApi(Resource):
tenant_count = TenantService.get_tenant_count()
if tenant_count > 0:
raise AlreadySetupError()
+
+ if not get_init_validate_status():
+ raise NotInitValidateError()
parser = reqparse.RequestParser()
parser.add_argument('email', type=email,
@@ -71,7 +75,10 @@ def setup_required(view):
@wraps(view)
def decorated(*args, **kwargs):
# check setup
- if not get_setup_status():
+ if not get_init_validate_status():
+ raise NotInitValidateError()
+
+ elif not get_setup_status():
raise NotSetupError()
return view(*args, **kwargs)
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 89301ee35a..d9d8ba8d52 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -15,6 +15,9 @@ services:
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_WEB_URL: ''
+ # Password for admin user initialization.
+ # If left unset, admin user will not be prompted for a password when creating the initial admin account.
+ INIT_PASSWORD: ''
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
diff --git a/web/app/init/InitPasswordPopup.tsx b/web/app/init/InitPasswordPopup.tsx
new file mode 100644
index 0000000000..545d4722b2
--- /dev/null
+++ b/web/app/init/InitPasswordPopup.tsx
@@ -0,0 +1,82 @@
+'use client'
+import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRouter } from 'next/navigation'
+import Toast from '../components/base/toast'
+import Loading from '../components/base/loading'
+import Button from '@/app/components/base/button'
+import { fetchInitValidateStatus, initValidate } from '@/service/common'
+import type { InitValidateStatusResponse } from '@/models/common'
+
+const InitPasswordPopup = () => {
+ const [password, setPassword] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [validated, setValidated] = useState(false)
+ const router = useRouter()
+
+ const { t } = useTranslation()
+
+ const handleValidation = async () => {
+ setLoading(true)
+ try {
+ const response = await initValidate({ body: { password } })
+ if (response.result === 'success') {
+ setValidated(true)
+ router.push('/install') // or render setup form
+ }
+ else {
+ throw new Error('Validation failed')
+ }
+ }
+ catch (e: any) {
+ Toast.notify({
+ type: 'error',
+ message: e.message,
+ duration: 5000,
+ })
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
+ if (res.status === 'finished')
+ window.location.href = '/install'
+ else
+ setLoading(false)
+ })
+ }, [])
+
+ return (
+ loading
+ ?