This commit is contained in:
seb
2026-10-29 17:32:39 +01:00
parent 867d550814
commit f555a1c9dd
6 changed files with 99 additions and 47 deletions

View File

@@ -8,12 +8,14 @@ export const auth = {
return {
user: data.user ?? null,
googleClientId: data.googleClientId ?? null,
schemaVersion: data.schemaVersion ?? null,
};
} catch (err) {
if (err.status === 401 || err.status === 403) {
return {
user: null,
googleClientId: err.data?.googleClientId ?? null,
schemaVersion: err.data?.schemaVersion ?? null,
forbidden: err.status === 403,
message: err.message,
};

View File

@@ -30,6 +30,7 @@ import { auth, company, number as numberApi } from '../api';
* @typedef {Object} AuthState
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
* @property {string|null} googleClientId Google Identity Services client ID.
* @property {number|null} schemaVersion Database schema version returned by the backend.
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
* @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.
@@ -47,6 +48,7 @@ export const AUTH_LOGGED_OUT = 'auth/loggedOut';
const initialState = {
user: null,
googleClientId: null,
schemaVersion: null,
status: 'loading',
hasAuth: false,
hasCompany: false,
@@ -137,22 +139,26 @@ function syncSse(state) {
function applySession(nextSession) {
const googleClientId =
nextSession.googleClientId ?? authAutomat.state.googleClientId;
const schemaVersion =
nextSession.schemaVersion !== undefined
? nextSession.schemaVersion
: (authAutomat.state.schemaVersion ?? null);
if (!nextSession.user) {
if (authAutomat.state.status !== 'authed') {
const nextState = authAutomat.setState(
buildAuthState(null, 'anon', { googleClientId, retryNotification: null })
buildAuthState(null, 'anon', { googleClientId, schemaVersion, retryNotification: null })
);
syncSse(nextState);
} else {
authAutomat.setState({ googleClientId, retryNotification: null });
authAutomat.setState({ googleClientId, schemaVersion, retryNotification: null });
}
return nextSession;
}
const user = mergeUser(authAutomat.state.user, nextSession.user);
const nextState = authAutomat.setState(
buildAuthState(user, 'authed', { googleClientId, retryNotification: null })
buildAuthState(user, 'authed', { googleClientId, schemaVersion, retryNotification: null })
);
syncSse(nextState);
emit({ type: AUTH_SESSION_ESTABLISHED });
@@ -234,6 +240,7 @@ const actions = {
const nextState = authAutomat.setState(
buildAuthState(user, 'authed', {
googleClientId: session.googleClientId ?? authAutomat.state.googleClientId,
schemaVersion: session.schemaVersion ?? authAutomat.state.schemaVersion ?? null,
})
);
syncSse(nextState);
@@ -344,6 +351,7 @@ export const selectHasCompany = () => Boolean(authAutomat.state.hasCompany);
export const selectSelectedCompanyId = () =>
authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null;
export const selectRetryNotification = () => authAutomat.state.retryNotification;
export const selectSchemaVersion = () => authAutomat.state.schemaVersion;
/**
* Re-reads `GET /api/auth/me` from the server.
@@ -351,7 +359,7 @@ export const selectRetryNotification = () => authAutomat.state.retryNotification
* Called after login, logout, and company switches so the automat reflects
* the current server session rather than an optimistic guess.
*
* @returns {Promise<{user: AuthUser|null, googleClientId: string|null}>} Session.
* @returns {Promise<{user: AuthUser|null, googleClientId: string|null, schemaVersion?: number|null}>} Session.
*/
export function bootstrapAuthMe() {
return authAutomat.actions.bootstrapSession();

View File

@@ -13,6 +13,8 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import CloseIcon from '@mui/icons-material/Close';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import authAutomat from '../../automats/authAutomat';
import CompanySettingsPanel from './CompanySettingsPanel';
import NumberSettingsPanel from './NumberSettingsPanel';
import PrinterSettingsPanel from './PrinterSettingsPanel';
@@ -54,15 +56,31 @@ const TABS = [
export default class SettingsDialog extends Component {
constructor(props) {
super(props);
this.state = { tab: TAB_KEYS.health };
this.state = {
tab: TAB_KEYS.health,
schemaVersion: authAutomat.state.schemaVersion,
};
this.handleTabChange = this.handleTabChange.bind(this);
}
componentDidMount() {
this.unsubAuth = authAutomat.subscribe(this, ['schemaVersion']);
}
componentWillUnmount() {
this.unsubAuth?.();
}
componentDidUpdate(prevProps) {
const { open } = this.props;
if (!open && prevProps.open) {
this.setState({ tab: TAB_KEYS.health });
}
if (open && !prevProps.open) {
if (this.state.schemaVersion !== authAutomat.state.schemaVersion) {
this.setState({ schemaVersion: authAutomat.state.schemaVersion });
}
}
}
handleTabChange(_event, value) {
@@ -71,7 +89,7 @@ export default class SettingsDialog extends Component {
render() {
const { open, onClose } = this.props;
const { tab } = this.state;
const { tab, schemaVersion } = this.state;
const paperSx = {
height: '85vh',
@@ -120,6 +138,9 @@ export default class SettingsDialog extends Component {
borderColor: 'divider',
bgcolor: SURFACE_BASE,
py: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
overflowY: 'auto',
}}
>
@@ -172,6 +193,13 @@ export default class SettingsDialog extends Component {
/>
))}
</Tabs>
{schemaVersion != null && (
<Box sx={{ px: 2.5, py: 1.5, mt: 'auto', borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', px: 0.5 }}>
DB-Version: {schemaVersion}
</Typography>
</Box>
)}
</Box>
{/* Right content panel */}