feat(v1.1.8): admin HTTP — /apps/{id}/users + /invitations

users_admin_api router merged into the guarded admin tree at
/api/v1/admin/apps/{id_or_slug}/users/* + /invitations/*.

Endpoints (capability gates applied via the service layer):

  GET    /users                                       AppUsersRead
  GET    /users/{user_id}                             AppUsersRead
  POST   /users                                       AppUsersWrite
  PATCH  /users/{user_id}                             AppUsersWrite
  DELETE /users/{user_id}                             AppUsersWrite (admins satisfy implicitly)
  POST   /users/{user_id}/reset-password              AppUsersAdmin -> one-shot token
  POST   /users/{user_id}/revoke-sessions             AppUsersAdmin
  GET    /invitations                                 AppUsersAdmin
  POST   /invitations                                 AppUsersAdmin
  DELETE /invitations/{invite_id}                     AppUsersAdmin

DTOs never include password_hash, session tokens, or unrotated
reset tokens. The reset-password endpoint returns the raw one-shot
token exactly once in the response body so an admin can paste it
into a manual reset link (TTL 1h by default).

Synth_cx() factors the admin-side cx construction into one place
(marked) so the SDK and admin code paths share the service's authz
fan-out. Admin-mediated trait methods (admin_create_invitation,
admin_reset_password_token, admin_revoke_all_sessions,
list_invitations, revoke_invitation) take &Principal directly,
not the synthesized cx.

UsersServiceImpl: removed the NOT_YET_IMPL constant + unused
auth alias (every method has a real implementation now). Added
Serialize+Deserialize on the AppUser / Invitation shared DTOs so
the admin HTTP layer doesn't need a parallel set of types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 12:18:29 +02:00
parent 3af99873c3
commit aa2631ff61
5 changed files with 621 additions and 23 deletions

View File

@@ -282,7 +282,7 @@ pub async fn build_app(
pubsub,
secrets,
email,
users,
users.clone(),
);
let engine = Arc::new(Engine::new(Limits::default(), services));
@@ -448,9 +448,14 @@ pub async fn build_app(
};
let app_members_state = AppMembersState {
apps: apps_state.apps.clone(),
users: auth.users,
users: auth.users.clone(),
members,
authz,
authz: authz.clone(),
};
let app_users_admin_state = picloud_manager_core::AppUsersState {
apps: apps_state.apps.clone(),
authz: authz.clone(),
users: users.clone(),
};
let api_keys_state = ApiKeysState { keys: auth.keys };
@@ -466,6 +471,7 @@ pub async fn build_app(
.merge(admins_router(admins_state))
.merge(apps_router(apps_state))
.merge(app_members_router(app_members_state))
.merge(picloud_manager_core::app_users_router(app_users_admin_state))
.merge(api_keys_router(api_keys_state))
.merge(triggers_router(triggers_state))
.merge(files_admin_router(files_admin_state))