diff --git a/backend/fluksio/api/routes/cloud.py b/backend/fluksio/api/routes/cloud.py
index b8f1fb2..029e342 100644
--- a/backend/fluksio/api/routes/cloud.py
+++ b/backend/fluksio/api/routes/cloud.py
@@ -281,19 +281,31 @@ def forget_remote_user(portal_sub: str) -> None:
@router.delete(
"", dependencies=[Depends(get_current_active_superuser)], response_model=Message
)
-def disconnect(request: Request) -> Message:
+def disconnect(request: Request, session: SessionDep) -> Message:
"""Sever the connection from this side.
Unilateral and immediate: the config is the only reason portal tokens
verify here, so deleting it ends remote access whatever the portal still
- has on file.
+ has on file. The users the portal let in go with it. The account it was
+ enrolled as stays, and so does every superuser.
"""
task = getattr(request.app.state, "cloud_task", None)
if task is not None:
task.cancel()
request.app.state.cloud_task = None
request.app.state.cloud_connector = None
+ config = cloud_config.load()
cloud_config.delete()
+ owner = config.local_user_id if config is not None else None
+ for user in session.exec(
+ select(User).where(
+ col(User.portal_sub).is_not(None),
+ User.is_superuser == False, # noqa: E712
+ )
+ ):
+ if str(user.id) != owner:
+ session.delete(user)
+ session.commit()
return Message(message="Disconnected from the portal")
diff --git a/backend/tests/test_cloud.py b/backend/tests/test_cloud.py
index 095479e..fd5a9a5 100644
--- a/backend/tests/test_cloud.py
+++ b/backend/tests/test_cloud.py
@@ -93,6 +93,41 @@ def test_disconnecting_ends_remote_access(
decode_token(portal_token(portal_key))
+def test_disconnecting_deletes_the_users_the_portal_let_in(
+ client: TestClient,
+ enrolled: User,
+ superuser_token_headers: dict[str, str],
+ db: Session,
+) -> None:
+ """Nothing reaches them once the portal is gone; the rest stay."""
+ member = User(email="member-dc@example.com", hashed_password="x", portal_sub="p-1")
+ boss = User(
+ email="boss-dc@example.com",
+ hashed_password="x",
+ portal_sub="p-2",
+ is_superuser=True,
+ )
+ plain = create_random_user(db)
+ db.add(member)
+ db.add(boss)
+ db.commit()
+ member_id, boss_id = member.id, boss.id
+
+ response = client.delete(
+ f"{settings.API_V1_STR}/cloud", headers=superuser_token_headers
+ )
+ assert response.status_code == 200, response.text
+ db.expire_all()
+ assert db.get(User, member_id) is None
+ assert db.get(User, boss_id) is not None
+ assert db.get(User, plain.id) is not None
+ assert db.get(User, enrolled.id) is not None
+ assert cloud_config.load() is None
+ db.delete(db.get(User, boss_id))
+ db.delete(db.get(User, plain.id))
+ db.commit()
+
+
def test_status_reports_not_enrolled(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
diff --git a/docs/interface/portal.md b/docs/interface/portal.md
index ed6c540..fa26941 100644
--- a/docs/interface/portal.md
+++ b/docs/interface/portal.md
@@ -83,7 +83,8 @@ proxy on that route must support WebSockets.
delete or deactivate their account under **Admin → Users**.
- To end all portal access from the instance, press **Disconnect**. This stops
the connection and invalidates portal credentials locally, without requiring
- the portal to be online.
+ the portal to be online. It also deletes the users the portal let in; the
+ account that enrolled and every superuser stay.
- To replace the pairing, use **Connect with a new code**. Remote users and
panels may need to pair again.
diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts
index 4c659a3..20e561b 100644
--- a/frontend/src/client/sdk.gen.ts
+++ b/frontend/src/client/sdk.gen.ts
@@ -469,7 +469,8 @@ export class CloudService {
*
* Unilateral and immediate: the config is the only reason portal tokens
* verify here, so deleting it ends remote access whatever the portal still
- * has on file.
+ * has on file. The users the portal let in go with it. The account it was
+ * enrolled as stays, and so does every superuser.
* @returns Message Successful Response
* @throws ApiError
*/
diff --git a/frontend/src/components/UserSettings/RemoteAccess.tsx b/frontend/src/components/UserSettings/RemoteAccess.tsx
index 994534a..c736ec7 100644
--- a/frontend/src/components/UserSettings/RemoteAccess.tsx
+++ b/frontend/src/components/UserSettings/RemoteAccess.tsx
@@ -104,6 +104,7 @@ export function RemoteAccess() {
setConfirmDisconnect(false)
showSuccessToast("Disconnected from the portal")
invalidate()
+ queryClient.invalidateQueries({ queryKey: ["users"] })
},
onError: handleError.bind(showErrorToast),
})
@@ -290,8 +291,9 @@ export function RemoteAccess() {
Disconnect from the portal?
Remote access ends immediately and the portal's credentials stop
- working here. Nothing on this instance is changed or deleted, and
- you can connect again with a new code.
+ working here. The users the portal let in are deleted. The account
+ it was connected as and every superuser stay, flows and data are
+ untouched, and you can connect again with a new code.