u
This commit is contained in:
@@ -31,6 +31,8 @@ import { auth, company, number as numberApi } from '../api';
|
|||||||
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
|
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
|
||||||
* @property {string|null} googleClientId Google Identity Services client ID.
|
* @property {string|null} googleClientId Google Identity Services client ID.
|
||||||
* @property {number|null} schemaVersion Database schema version returned by the backend.
|
* @property {number|null} schemaVersion Database schema version returned by the backend.
|
||||||
|
* @property {boolean} vhost Whether vhost mode is enabled.
|
||||||
|
* @property {string|null} vhostSettings Vhost configuration string (e.g. posport:41420;posip:147.135.129.77).
|
||||||
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
||||||
* @property {boolean} hasAuth True when the session has a valid authenticated user.
|
* @property {boolean} hasAuth True when the session has a valid authenticated user.
|
||||||
* @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected.
|
* @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected.
|
||||||
@@ -49,6 +51,8 @@ const initialState = {
|
|||||||
user: null,
|
user: null,
|
||||||
googleClientId: null,
|
googleClientId: null,
|
||||||
schemaVersion: null,
|
schemaVersion: null,
|
||||||
|
vhost: false,
|
||||||
|
vhostSettings: null,
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
hasAuth: false,
|
hasAuth: false,
|
||||||
hasCompany: false,
|
hasCompany: false,
|
||||||
@@ -124,6 +128,8 @@ function buildAuthState(user, status, extra = {}) {
|
|||||||
status,
|
status,
|
||||||
hasAuth,
|
hasAuth,
|
||||||
hasCompany,
|
hasCompany,
|
||||||
|
vhost: authAutomat?.state?.vhost ?? false,
|
||||||
|
vhostSettings: authAutomat?.state?.vhostSettings ?? null,
|
||||||
retryNotification: null,
|
retryNotification: null,
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
@@ -143,22 +149,30 @@ function applySession(nextSession) {
|
|||||||
nextSession.schemaVersion !== undefined
|
nextSession.schemaVersion !== undefined
|
||||||
? nextSession.schemaVersion
|
? nextSession.schemaVersion
|
||||||
: (authAutomat.state.schemaVersion ?? null);
|
: (authAutomat.state.schemaVersion ?? null);
|
||||||
|
const vhost =
|
||||||
|
nextSession.vhost !== undefined
|
||||||
|
? Boolean(nextSession.vhost)
|
||||||
|
: (authAutomat.state.vhost ?? false);
|
||||||
|
const vhostSettings =
|
||||||
|
nextSession.vhostSettings !== undefined
|
||||||
|
? nextSession.vhostSettings
|
||||||
|
: (authAutomat.state.vhostSettings ?? null);
|
||||||
|
|
||||||
if (!nextSession.user) {
|
if (!nextSession.user) {
|
||||||
if (authAutomat.state.status !== 'authed') {
|
if (authAutomat.state.status !== 'authed') {
|
||||||
const nextState = authAutomat.setState(
|
const nextState = authAutomat.setState(
|
||||||
buildAuthState(null, 'anon', { googleClientId, schemaVersion, retryNotification: null })
|
buildAuthState(null, 'anon', { googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null })
|
||||||
);
|
);
|
||||||
syncSse(nextState);
|
syncSse(nextState);
|
||||||
} else {
|
} else {
|
||||||
authAutomat.setState({ googleClientId, schemaVersion, retryNotification: null });
|
authAutomat.setState({ googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null });
|
||||||
}
|
}
|
||||||
return nextSession;
|
return nextSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||||
const nextState = authAutomat.setState(
|
const nextState = authAutomat.setState(
|
||||||
buildAuthState(user, 'authed', { googleClientId, schemaVersion, retryNotification: null })
|
buildAuthState(user, 'authed', { googleClientId, schemaVersion, vhost, vhostSettings, retryNotification: null })
|
||||||
);
|
);
|
||||||
syncSse(nextState);
|
syncSse(nextState);
|
||||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||||
@@ -352,6 +366,8 @@ export const selectSelectedCompanyId = () =>
|
|||||||
authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null;
|
authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null;
|
||||||
export const selectRetryNotification = () => authAutomat.state.retryNotification;
|
export const selectRetryNotification = () => authAutomat.state.retryNotification;
|
||||||
export const selectSchemaVersion = () => authAutomat.state.schemaVersion;
|
export const selectSchemaVersion = () => authAutomat.state.schemaVersion;
|
||||||
|
export const selectVhost = () => Boolean(authAutomat.state.vhost);
|
||||||
|
export const selectVhostSettings = () => authAutomat.state.vhostSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-reads `GET /api/auth/me` from the server.
|
* Re-reads `GET /api/auth/me` from the server.
|
||||||
|
|||||||
@@ -19,12 +19,29 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
|||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import BackendError from '../BackendError';
|
import BackendError from '../BackendError';
|
||||||
import CategoryPickerDialog from '../article/CategoryPickerDialog';
|
import CategoryPickerDialog from '../article/CategoryPickerDialog';
|
||||||
|
import authAutomat from '../../automats/authAutomat';
|
||||||
import companyAutomat from '../../automats/companyAutomat';
|
import companyAutomat from '../../automats/companyAutomat';
|
||||||
import categoryAutomat, { whenCategoryTreeReady } from '../../automats/categoryAutomat';
|
import categoryAutomat, { whenCategoryTreeReady } from '../../automats/categoryAutomat';
|
||||||
import { categoryDisplayName } from '../../utils/categoryUtils';
|
import { categoryDisplayName } from '../../utils/categoryUtils';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
|
|
||||||
|
function parseVhostSettings(str) {
|
||||||
|
if (!str || typeof str !== 'string') return {};
|
||||||
|
const result = {};
|
||||||
|
for (const part of str.split(';')) {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const colonIdx = trimmed.indexOf(':');
|
||||||
|
if (colonIdx !== -1) {
|
||||||
|
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
|
||||||
|
const val = trimmed.slice(colonIdx + 1).trim();
|
||||||
|
result[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(val) {
|
function formatDateTime(val) {
|
||||||
if (!val) return null;
|
if (!val) return null;
|
||||||
try {
|
try {
|
||||||
@@ -52,6 +69,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
terminals,
|
terminals,
|
||||||
categories: categoryAutomat.state.categories,
|
categories: categoryAutomat.state.categories,
|
||||||
rootCategoryId: categoryAutomat.state.rootCategoryId,
|
rootCategoryId: categoryAutomat.state.rootCategoryId,
|
||||||
|
vhostSettings: authAutomat.state.vhostSettings,
|
||||||
newTerminalName: '',
|
newTerminalName: '',
|
||||||
creatingTerminal: false,
|
creatingTerminal: false,
|
||||||
enableAllForNew: true,
|
enableAllForNew: true,
|
||||||
@@ -82,6 +100,9 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
categories: state.categories,
|
categories: state.categories,
|
||||||
rootCategoryId: state.rootCategoryId,
|
rootCategoryId: state.rootCategoryId,
|
||||||
}));
|
}));
|
||||||
|
this.unsubAuth = authAutomat.subscribe(this, (state) => ({
|
||||||
|
vhostSettings: state.vhostSettings,
|
||||||
|
}));
|
||||||
if (categoryAutomat.state.treeStatus === 'idle') {
|
if (categoryAutomat.state.treeStatus === 'idle') {
|
||||||
void categoryAutomat.actions.loadTree();
|
void categoryAutomat.actions.loadTree();
|
||||||
}
|
}
|
||||||
@@ -90,6 +111,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsub?.();
|
this.unsub?.();
|
||||||
this.unsubCategory?.();
|
this.unsubCategory?.();
|
||||||
|
this.unsubAuth?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTerminals() {
|
async loadTerminals() {
|
||||||
@@ -275,6 +297,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
terminals,
|
terminals,
|
||||||
|
vhostSettings,
|
||||||
newTerminalName,
|
newTerminalName,
|
||||||
creatingTerminal,
|
creatingTerminal,
|
||||||
enableAllForNew,
|
enableAllForNew,
|
||||||
@@ -284,6 +307,11 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
rootCategoryId,
|
rootCategoryId,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const parsedVhost = parseVhostSettings(vhostSettings);
|
||||||
|
const posIp = parsedVhost.posip || parsedVhost.ip || null;
|
||||||
|
const posPort = parsedVhost.posport || parsedVhost.port || null;
|
||||||
|
const hasVhostSettings = Boolean(posIp || posPort || vhostSettings);
|
||||||
|
|
||||||
if (status === 'loading' && terminals.length === 0) {
|
if (status === 'loading' && terminals.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
@@ -298,6 +326,36 @@ export default class PosTerminalSettingsPanel extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ maxWidth: 680, mx: 'auto', py: 2 }}>
|
<Box sx={{ maxWidth: 680, mx: 'auto', py: 2 }}>
|
||||||
|
{hasVhostSettings && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
p: 1.5,
|
||||||
|
px: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||||
|
Pos Sync IP:{' '}
|
||||||
|
<Box component="span" sx={{ color: 'text.primary', fontFamily: 'monospace', fontWeight: 600 }}>
|
||||||
|
{posIp ?? '—'}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||||
|
Pos Sync Port:{' '}
|
||||||
|
<Box component="span" sx={{ color: 'text.primary', fontFamily: 'monospace', fontWeight: 600 }}>
|
||||||
|
{posPort ?? '—'}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -53,18 +53,34 @@ const TABS = [
|
|||||||
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getDefaultTab(vhost) {
|
||||||
|
return vhost ? TAB_KEYS.company : TAB_KEYS.health;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleTabs(vhost) {
|
||||||
|
if (!vhost) return TABS;
|
||||||
|
return TABS.filter(
|
||||||
|
(t) => t.id !== TAB_KEYS.health && t.id !== TAB_KEYS.backup && t.id !== TAB_KEYS.printers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default class SettingsDialog extends Component {
|
export default class SettingsDialog extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const vhost = Boolean(authAutomat.state.vhost);
|
||||||
this.state = {
|
this.state = {
|
||||||
tab: TAB_KEYS.health,
|
tab: getDefaultTab(vhost),
|
||||||
schemaVersion: authAutomat.state.schemaVersion,
|
schemaVersion: authAutomat.state.schemaVersion,
|
||||||
|
vhost,
|
||||||
};
|
};
|
||||||
this.handleTabChange = this.handleTabChange.bind(this);
|
this.handleTabChange = this.handleTabChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsubAuth = authAutomat.subscribe(this, ['schemaVersion']);
|
this.unsubAuth = authAutomat.subscribe(this, (state) => ({
|
||||||
|
schemaVersion: state.schemaVersion,
|
||||||
|
vhost: Boolean(state.vhost),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -73,12 +89,29 @@ export default class SettingsDialog extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { open } = this.props;
|
const { open } = this.props;
|
||||||
|
const { vhost } = this.state;
|
||||||
if (!open && prevProps.open) {
|
if (!open && prevProps.open) {
|
||||||
this.setState({ tab: TAB_KEYS.health });
|
this.setState({ tab: getDefaultTab(vhost) });
|
||||||
}
|
}
|
||||||
if (open && !prevProps.open) {
|
if (open && !prevProps.open) {
|
||||||
|
const currentVhost = Boolean(authAutomat.state.vhost);
|
||||||
|
const updates = {};
|
||||||
if (this.state.schemaVersion !== authAutomat.state.schemaVersion) {
|
if (this.state.schemaVersion !== authAutomat.state.schemaVersion) {
|
||||||
this.setState({ schemaVersion: authAutomat.state.schemaVersion });
|
updates.schemaVersion = authAutomat.state.schemaVersion;
|
||||||
|
}
|
||||||
|
if (this.state.vhost !== currentVhost) {
|
||||||
|
updates.vhost = currentVhost;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
currentVhost &&
|
||||||
|
(this.state.tab === TAB_KEYS.health ||
|
||||||
|
this.state.tab === TAB_KEYS.backup ||
|
||||||
|
this.state.tab === TAB_KEYS.printers)
|
||||||
|
) {
|
||||||
|
updates.tab = getDefaultTab(currentVhost);
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
this.setState(updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +122,9 @@ export default class SettingsDialog extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose } = this.props;
|
const { open, onClose } = this.props;
|
||||||
const { tab, schemaVersion } = this.state;
|
const { tab, schemaVersion, vhost } = this.state;
|
||||||
|
const visibleTabs = getVisibleTabs(vhost);
|
||||||
|
const activeTab = visibleTabs.some((t) => t.id === tab) ? tab : visibleTabs[0]?.id;
|
||||||
|
|
||||||
const paperSx = {
|
const paperSx = {
|
||||||
height: '85vh',
|
height: '85vh',
|
||||||
@@ -146,7 +181,7 @@ export default class SettingsDialog extends Component {
|
|||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
value={tab}
|
value={activeTab}
|
||||||
onChange={this.handleTabChange}
|
onChange={this.handleTabChange}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiTabs-indicator': {
|
'& .MuiTabs-indicator': {
|
||||||
@@ -182,7 +217,7 @@ export default class SettingsDialog extends Component {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{TABS.map((t) => (
|
{visibleTabs.map((t) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={t.id}
|
key={t.id}
|
||||||
disableRipple
|
disableRipple
|
||||||
@@ -205,15 +240,15 @@ export default class SettingsDialog extends Component {
|
|||||||
{/* Right content panel */}
|
{/* Right content panel */}
|
||||||
<DialogContent sx={{ flex: 1, minHeight: 0, p: 3, overflowY: 'auto' }}>
|
<DialogContent sx={{ flex: 1, minHeight: 0, p: 3, overflowY: 'auto' }}>
|
||||||
<Box sx={{ display: 'grid' }}>
|
<Box sx={{ display: 'grid' }}>
|
||||||
{TABS.map((t) => (
|
{visibleTabs.map((t) => (
|
||||||
<Box
|
<Box
|
||||||
key={t.id}
|
key={t.id}
|
||||||
sx={{
|
sx={{
|
||||||
gridArea: '1 / 1',
|
gridArea: '1 / 1',
|
||||||
display: t.id === tab ? 'block' : 'none',
|
display: t.id === activeTab ? 'block' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<t.Panel active={t.id === tab} />
|
<t.Panel active={t.id === activeTab} />
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user