An expired token was indistinguishable from a working one. set_token()
stamped token_expiry = now + 30 days every time a stored token was loaded,
so the expiry reset on every request and is_authenticated could never
return False. /cloud/status answered "connected" for as long as any token
existed, while every cloud call 401'd — and the user was shown Bambu's own
{"error": "Please login."} verbatim.
Bambu is now the authority: /cloud/status validates the token upstream
(cached 5m), and any 401 from any authenticated call durably records the
credential as dead via users.cloud_token_invalid_at, so MakerWorld, cloud
profiles, slicer presets and firmware checks all agree at once. An
unreachable Bambu is treated as unknown, never as expired, so an outage
cannot sign a working session out.
The user-facing message now names the Profiles page, where the Bambu Cloud
sign-in actually lives; the old text pointed at a Settings page that does
not exist. Same stale path corrected in the wiki.
feat(cloud): support China region for token-based login
The /cloud/token endpoint always used the global Bambu API endpoint,
so users with China-region access tokens could not validate their
token. The password login flow already exposes a region selector; this
brings the token flow to parity.
Users can authenticate against an LDAP/AD server with configurable
server URL, bind DN, search base, and user filter. Supports StartTLS
and LDAPS — plaintext is not allowed. Both Active Directory (memberOf)
and POSIX groups (memberUid) are mapped to BamBuddy groups on each
login. Auto-provisioning creates local accounts on first LDAP login.
Local admin accounts remain as fallback when LDAP is unreachable.
Password management is disabled for LDAP users.
Cloud credentials were stored globally — one Bambu Cloud account per
Bambuddy instance. When auth was enabled, any user logging into Cloud
overwrote everyone else's credentials. Credentials are now stored
per-user: each user gets their own independent Cloud login.
Also fixed cloud data endpoints (settings, fields, preset CRUD)
requiring settings:read/settings:update permissions instead of
cloud:auth — users who had "Cloud Auth" enabled but "Settings"
disabled couldn't load profiles after logging in.
Implement a full permissions system replacing simple admin/user roles:
Backend:
- Add Group model with many-to-many user relationship
- Add 50+ granular permissions (resource:action pattern)
- Create default groups: Administrators, Operators, Viewers
- Add permission-checking dependencies for route protection
- Add groups API endpoints (CRUD, user assignment)
- Add change password endpoint for users
- Update backup/restore to include groups
- Migrate existing users to groups on startup
Frontend:
- Add GroupsPage for managing groups and permissions
- Add permission helpers to AuthContext (hasPermission, hasAnyPermission)
- Add PermissionRoute component for protected routes
- Disable buttons/features based on permissions (with tooltips)
- Add change password modal in sidebar for all users
- Add forgot password info modal on login page
- Show user groups in UsersPage with group assignment
Testing:
- Add integration tests for groups API
- Add tests for user-group assignments
- Add tests for change password endpoint
- Seed default groups in test fixtures
Closes#28#161