Make auth toggle a settings option instead of env var
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
Some checks failed
CI / style (push) Has been cancelled
CI / build-client (push) Has been cancelled
CI / build-tester (push) Has been cancelled
CI / build-amd64 (push) Has been cancelled
CI / test (cockroachdb) (push) Has been cancelled
CI / test (mariadb) (push) Has been cancelled
CI / test (postgres) (push) Has been cancelled
CI / test (sqlite) (push) Has been cancelled
CI / build-arm64 (push) Has been cancelled
CI / build-armv7 (push) Has been cancelled
CI / publish-images (push) Has been cancelled
CI / publish-release (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
- Auth enabled state now stored in database setting (auth_enabled) - Add POST /auth/toggle endpoint to enable/disable auth from UI - Add auth toggle section to Settings > General page - Enabling auth without users prompts to create first admin - Disabling auth requires admin password confirmation - Auth setting cached in memory and loaded on startup Removes need for SPOOLMAN_AUTH_ENABLED env var Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -390,6 +390,22 @@
|
|||||||
"replace_warning_title": "Replace all data?",
|
"replace_warning_title": "Replace all data?",
|
||||||
"replace_warning": "This will DELETE all existing data and replace it with the backup. This cannot be undone!"
|
"replace_warning": "This will DELETE all existing data and replace it with the backup. This cannot be undone!"
|
||||||
},
|
},
|
||||||
|
"auth": {
|
||||||
|
"title": "Authentication",
|
||||||
|
"description": "Require users to log in to access Spoolman. Once enabled, you'll be redirected to the login page.",
|
||||||
|
"status_enabled": "Authentication is enabled",
|
||||||
|
"status_disabled": "Authentication is disabled",
|
||||||
|
"setup_title": "Enable Authentication",
|
||||||
|
"setup_description": "Create an admin account to enable authentication. This user will have full access to manage Spoolman.",
|
||||||
|
"enable_button": "Create Admin & Enable",
|
||||||
|
"disable_title": "Disable Authentication",
|
||||||
|
"disable_description": "Enter an admin password to confirm disabling authentication. Anyone will be able to access Spoolman without logging in.",
|
||||||
|
"disable_button": "Disable Authentication",
|
||||||
|
"admin_password": "Admin Password",
|
||||||
|
"password_required": "Password is required",
|
||||||
|
"enabled_success": "Authentication enabled",
|
||||||
|
"disabled_success": "Authentication disabled"
|
||||||
|
},
|
||||||
"extra_fields": {
|
"extra_fields": {
|
||||||
"tab": "Extra Fields",
|
"tab": "Extra Fields",
|
||||||
"description": "<p>Here you can add extra custom fields to your entities.</p><p>Once a field is added, you can not change its key or type, and for choice type fields you can not remove choices or change the multi choice state. If you remove a field, the associated data for all entities will be deleted.</p><p>The key is what other programs read/write the data as, so if your custom field is supposed to integrate with a third-party program, make sure to set it correctly. Default value is only applied to new items.</p><p>Extra fields can not be sorted or filtered in the table views.</p>",
|
"description": "<p>Here you can add extra custom fields to your entities.</p><p>Once a field is added, you can not change its key or type, and for choice type fields you can not remove choices or change the multi choice state. If you remove a field, the associated data for all entities will be deleted.</p><p>The key is what other programs read/write the data as, so if your custom field is supposed to integrate with a third-party program, make sure to set it correctly. Default value is only applied to new items.</p><p>Extra fields can not be sorted or filtered in the table views.</p>",
|
||||||
@@ -449,7 +465,7 @@
|
|||||||
"role_editor": "Editor - Can view, use, create, and edit",
|
"role_editor": "Editor - Can view, use, create, and edit",
|
||||||
"role_admin": "Admin - Full access including user management",
|
"role_admin": "Admin - Full access including user management",
|
||||||
"auth_disabled": "Authentication is not enabled",
|
"auth_disabled": "Authentication is not enabled",
|
||||||
"auth_disabled_hint": "Set SPOOLMAN_AUTH_ENABLED=true to enable user management",
|
"auth_disabled_hint": "Enable authentication in Settings > General to manage users",
|
||||||
"admin_required": "Admin Access Required",
|
"admin_required": "Admin Access Required",
|
||||||
"admin_required_hint": "Only administrators can manage users",
|
"admin_required_hint": "Only administrators can manage users",
|
||||||
"logout": "Log Out"
|
"logout": "Log Out"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { DownloadOutlined, UploadOutlined } from "@ant-design/icons";
|
import { DownloadOutlined, LockOutlined, UploadOutlined } from "@ant-design/icons";
|
||||||
import { useTranslate } from "@refinedev/core";
|
import { useTranslate } from "@refinedev/core";
|
||||||
import { Button, Checkbox, Divider, Form, Input, message, Popconfirm, Space, Upload } from "antd";
|
import { Button, Checkbox, Divider, Form, Input, message, Modal, Popconfirm, Space, Switch, Upload } from "antd";
|
||||||
import type { UploadFile } from "antd";
|
import type { UploadFile } from "antd";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { getAPIURL } from "../../utils/url";
|
import { getAPIURL } from "../../utils/url";
|
||||||
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
|
import { useGetSettings, useSetSetting } from "../../utils/querySettings";
|
||||||
import { ColorModeContext, THEME_COLORS, ThemeColorKey } from "../../contexts/color-mode";
|
import { ColorModeContext, THEME_COLORS, ThemeColorKey } from "../../contexts/color-mode";
|
||||||
|
import { useAuth } from "../../contexts/auth";
|
||||||
|
|
||||||
export function GeneralSettings() {
|
export function GeneralSettings() {
|
||||||
const settings = useGetSettings();
|
const settings = useGetSettings();
|
||||||
@@ -137,6 +138,10 @@ export function GeneralSettings() {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
<AuthSettings messageApi={messageApi} t={t} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<BackupRestore messageApi={messageApi} t={t} />
|
<BackupRestore messageApi={messageApi} t={t} />
|
||||||
|
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -144,6 +149,176 @@ export function GeneralSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthSettingsProps {
|
||||||
|
messageApi: ReturnType<typeof message.useMessage>[0];
|
||||||
|
t: ReturnType<typeof useTranslate>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthSettings({ messageApi, t }: AuthSettingsProps) {
|
||||||
|
const { authStatus, refreshAuthStatus, setup } = useAuth();
|
||||||
|
const [toggling, setToggling] = useState(false);
|
||||||
|
const [setupModalOpen, setSetupModalOpen] = useState(false);
|
||||||
|
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||||
|
const [setupForm] = Form.useForm();
|
||||||
|
const [disableForm] = Form.useForm();
|
||||||
|
|
||||||
|
const handleToggle = async (enabled: boolean) => {
|
||||||
|
if (enabled) {
|
||||||
|
// If enabling and no users exist, show setup modal
|
||||||
|
if (!authStatus?.has_users) {
|
||||||
|
setSetupModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If users exist, just enable
|
||||||
|
await toggleAuth(true);
|
||||||
|
} else {
|
||||||
|
// Disabling - show password confirmation modal
|
||||||
|
setDisableModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAuth = async (enabled: boolean, password?: string) => {
|
||||||
|
setToggling(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${getAPIURL()}/auth/toggle`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ enabled, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.detail || "Failed to toggle authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshAuthStatus();
|
||||||
|
messageApi.success(
|
||||||
|
enabled
|
||||||
|
? t("settings.auth.enabled_success")
|
||||||
|
: t("settings.auth.disabled_success")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Redirect to login after enabling
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
messageApi.error(err instanceof Error ? err.message : "Failed to toggle authentication");
|
||||||
|
} finally {
|
||||||
|
setToggling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetup = async (values: { username: string; password: string }) => {
|
||||||
|
setToggling(true);
|
||||||
|
try {
|
||||||
|
// Create the first admin user
|
||||||
|
await setup(values.username, values.password);
|
||||||
|
// Enable auth
|
||||||
|
await toggleAuth(true);
|
||||||
|
setSetupModalOpen(false);
|
||||||
|
setupForm.resetFields();
|
||||||
|
} catch (err) {
|
||||||
|
messageApi.error(err instanceof Error ? err.message : "Setup failed");
|
||||||
|
} finally {
|
||||||
|
setToggling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisable = async (values: { password: string }) => {
|
||||||
|
await toggleAuth(false, values.password);
|
||||||
|
setDisableModalOpen(false);
|
||||||
|
disableForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
|
||||||
|
<h3>
|
||||||
|
<LockOutlined style={{ marginRight: 8 }} />
|
||||||
|
{t("settings.auth.title")}
|
||||||
|
</h3>
|
||||||
|
<p style={{ marginBottom: "1em", color: "#888" }}>
|
||||||
|
{t("settings.auth.description")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||||
|
<Switch
|
||||||
|
checked={authStatus?.enabled ?? false}
|
||||||
|
onChange={handleToggle}
|
||||||
|
loading={toggling}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{authStatus?.enabled
|
||||||
|
? t("settings.auth.status_enabled")
|
||||||
|
: t("settings.auth.status_disabled")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Setup Modal - Create first admin user */}
|
||||||
|
<Modal
|
||||||
|
title={t("settings.auth.setup_title")}
|
||||||
|
open={setupModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setSetupModalOpen(false);
|
||||||
|
setupForm.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => setupForm.submit()}
|
||||||
|
okText={t("settings.auth.enable_button")}
|
||||||
|
confirmLoading={toggling}
|
||||||
|
>
|
||||||
|
<p style={{ marginBottom: 16 }}>{t("settings.auth.setup_description")}</p>
|
||||||
|
<Form form={setupForm} layout="vertical" onFinish={handleSetup}>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label={t("settings.users.username")}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t("settings.users.username_required") },
|
||||||
|
{ min: 3, message: t("settings.users.username_min") },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label={t("settings.users.password")}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t("settings.users.password_required") },
|
||||||
|
{ min: 6, message: t("settings.users.password_min") },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Disable Modal - Require password confirmation */}
|
||||||
|
<Modal
|
||||||
|
title={t("settings.auth.disable_title")}
|
||||||
|
open={disableModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setDisableModalOpen(false);
|
||||||
|
disableForm.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => disableForm.submit()}
|
||||||
|
okText={t("settings.auth.disable_button")}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
confirmLoading={toggling}
|
||||||
|
>
|
||||||
|
<p style={{ marginBottom: 16 }}>{t("settings.auth.disable_description")}</p>
|
||||||
|
<Form form={disableForm} layout="vertical" onFinish={handleDisable}>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label={t("settings.auth.admin_password")}
|
||||||
|
rules={[{ required: true, message: t("settings.auth.password_required") }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface BackupRestoreProps {
|
interface BackupRestoreProps {
|
||||||
messageApi: ReturnType<typeof message.useMessage>[0];
|
messageApi: ReturnType<typeof message.useMessage>[0];
|
||||||
t: ReturnType<typeof useTranslate>;
|
t: ReturnType<typeof useTranslate>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "spoolman"
|
name = "spoolman"
|
||||||
version = "0.23C.4"
|
version = "0.23C.5"
|
||||||
description = "A web service that keeps track of 3D printing spools."
|
description = "A web service that keeps track of 3D printing spools."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Tony", email = "tony@nastynas.xyz" },
|
{ name = "Tony", email = "tony@nastynas.xyz" },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Authentication API endpoints."""
|
"""Authentication API endpoints."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
@@ -22,9 +23,11 @@ from spoolman.auth import (
|
|||||||
get_password_hash,
|
get_password_hash,
|
||||||
get_user_by_username,
|
get_user_by_username,
|
||||||
is_auth_enabled,
|
is_auth_enabled,
|
||||||
|
set_auth_enabled_cache,
|
||||||
)
|
)
|
||||||
from spoolman.database import models
|
from spoolman.database import models
|
||||||
from spoolman.database.database import get_db_session
|
from spoolman.database.database import get_db_session
|
||||||
|
from spoolman.settings import SETTINGS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -287,3 +290,85 @@ async def delete_user(
|
|||||||
|
|
||||||
logger.info("User deleted: %s", user.username)
|
logger.info("User deleted: %s", user.username)
|
||||||
return {"message": "User deleted"}
|
return {"message": "User deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
class AuthToggleRequest(BaseModel):
|
||||||
|
"""Request to enable or disable authentication."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
password: Optional[str] = Field(None, description="Admin password required when disabling auth")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/toggle",
|
||||||
|
name="Toggle authentication",
|
||||||
|
description="Enable or disable authentication. Requires admin password to disable.",
|
||||||
|
response_model=AuthStatusResponse,
|
||||||
|
)
|
||||||
|
async def toggle_auth(
|
||||||
|
body: AuthToggleRequest,
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||||
|
) -> AuthStatusResponse:
|
||||||
|
"""Enable or disable authentication."""
|
||||||
|
# Get current user count
|
||||||
|
result = await db.execute(select(func.count()).select_from(models.User))
|
||||||
|
user_count = result.scalar() or 0
|
||||||
|
|
||||||
|
if body.enabled:
|
||||||
|
# Enabling auth
|
||||||
|
if user_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot enable auth without users. Use /auth/setup to create the first admin user.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Disabling auth - require admin password for security
|
||||||
|
if not body.password:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Admin password required to disable authentication",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find any admin user and verify password
|
||||||
|
result = await db.execute(
|
||||||
|
select(models.User).where(models.User.role == Role.ADMIN.value, models.User.is_active == True) # noqa: E712
|
||||||
|
)
|
||||||
|
admin_users = result.scalars().all()
|
||||||
|
|
||||||
|
if not admin_users:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="No active admin user found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if password matches any admin
|
||||||
|
password_valid = False
|
||||||
|
for admin in admin_users:
|
||||||
|
if auth.verify_password(body.password, admin.hashed_password):
|
||||||
|
password_valid = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not password_valid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid admin password",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the setting in database
|
||||||
|
setting = models.Setting(
|
||||||
|
key="auth_enabled",
|
||||||
|
value=json.dumps(body.enabled),
|
||||||
|
last_updated=datetime.utcnow().replace(microsecond=0),
|
||||||
|
)
|
||||||
|
await db.merge(setting)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# Update the cache
|
||||||
|
set_auth_enabled_cache(body.enabled)
|
||||||
|
|
||||||
|
logger.info("Authentication %s", "enabled" if body.enabled else "disabled")
|
||||||
|
|
||||||
|
return AuthStatusResponse(
|
||||||
|
enabled=body.enabled,
|
||||||
|
has_users=user_count > 0,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Authentication utilities and dependencies."""
|
"""Authentication utilities and dependencies."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
@@ -31,10 +32,35 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|||||||
# OAuth2 scheme - auto_error=False allows unauthenticated access when auth is disabled
|
# OAuth2 scheme - auto_error=False allows unauthenticated access when auth is disabled
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login", auto_error=False)
|
||||||
|
|
||||||
|
# Cached auth enabled state (loaded from database on startup)
|
||||||
|
_auth_enabled_cache: bool = False
|
||||||
|
|
||||||
|
|
||||||
def is_auth_enabled() -> bool:
|
def is_auth_enabled() -> bool:
|
||||||
"""Check if authentication is enabled via environment variable."""
|
"""Check if authentication is enabled (from cached database setting)."""
|
||||||
return os.environ.get("SPOOLMAN_AUTH_ENABLED", "false").lower() in ("true", "1", "yes")
|
return _auth_enabled_cache
|
||||||
|
|
||||||
|
|
||||||
|
def set_auth_enabled_cache(enabled: bool) -> None:
|
||||||
|
"""Update the cached auth enabled state."""
|
||||||
|
global _auth_enabled_cache
|
||||||
|
_auth_enabled_cache = enabled
|
||||||
|
logger.info("Auth enabled cache updated: %s", enabled)
|
||||||
|
|
||||||
|
|
||||||
|
async def load_auth_enabled_from_db(db: AsyncSession) -> bool:
|
||||||
|
"""Load auth_enabled setting from database and update cache."""
|
||||||
|
from spoolman.settings import SETTINGS
|
||||||
|
|
||||||
|
setting = await db.get(models.Setting, "auth_enabled")
|
||||||
|
if setting is not None:
|
||||||
|
enabled = json.loads(setting.value)
|
||||||
|
else:
|
||||||
|
# Use default from settings definition
|
||||||
|
enabled = json.loads(SETTINGS["auth_enabled"].default)
|
||||||
|
|
||||||
|
set_auth_enabled_cache(enabled)
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
class Role(str, Enum):
|
class Role(str, Enum):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from fastapi.responses import PlainTextResponse, RedirectResponse, Response
|
|||||||
from prometheus_client import generate_latest
|
from prometheus_client import generate_latest
|
||||||
from scheduler.asyncio.scheduler import Scheduler
|
from scheduler.asyncio.scheduler import Scheduler
|
||||||
|
|
||||||
from spoolman import env, externaldb
|
from spoolman import auth, env, externaldb
|
||||||
from spoolman.api.v1.router import app as v1_app
|
from spoolman.api.v1.router import app as v1_app
|
||||||
from spoolman.client import SinglePageApplication
|
from spoolman.client import SinglePageApplication
|
||||||
from spoolman.database import database
|
from spoolman.database import database
|
||||||
@@ -173,6 +173,12 @@ async def startup() -> None:
|
|||||||
project_root = Path(__file__).parent.parent
|
project_root = Path(__file__).parent.parent
|
||||||
subprocess.run(["alembic", "upgrade", "head"], check=True, cwd=project_root) # noqa: S603, S607, ASYNC221
|
subprocess.run(["alembic", "upgrade", "head"], check=True, cwd=project_root) # noqa: S603, S607, ASYNC221
|
||||||
|
|
||||||
|
# Load auth enabled setting from database
|
||||||
|
logger.info("Loading auth settings...")
|
||||||
|
async for db in database.get_db_session():
|
||||||
|
auth_enabled = await auth.load_auth_enabled_from_db(db)
|
||||||
|
logger.info("Authentication enabled: %s", auth_enabled)
|
||||||
|
|
||||||
# Setup scheduler
|
# Setup scheduler
|
||||||
schedule = Scheduler()
|
schedule = Scheduler()
|
||||||
database.schedule_tasks(schedule)
|
database.schedule_tasks(schedule)
|
||||||
|
|||||||
@@ -72,3 +72,4 @@ register_setting("base_url", SettingType.STRING, json.dumps(""))
|
|||||||
|
|
||||||
register_setting("locations", SettingType.ARRAY, json.dumps([]))
|
register_setting("locations", SettingType.ARRAY, json.dumps([]))
|
||||||
register_setting("locations_spoolorders", SettingType.OBJECT, json.dumps({}))
|
register_setting("locations_spoolorders", SettingType.OBJECT, json.dumps({}))
|
||||||
|
register_setting("auth_enabled", SettingType.BOOLEAN, json.dumps(False))
|
||||||
|
|||||||
Reference in New Issue
Block a user