Skip to content

feat: Add simplified admin API for circle/team management#2434

Open
Sam428-png wants to merge 3 commits intonextcloud:masterfrom
Sam428-png:feat/admin-manage-api
Open

feat: Add simplified admin API for circle/team management#2434
Sam428-png wants to merge 3 commits intonextcloud:masterfrom
Sam428-png:feat/admin-manage-api

Conversation

@Sam428-png
Copy link
Copy Markdown

Summary

  • Adds /manage/ API endpoints that allow Nextcloud admins to fully manage circles (teams) without needing to be a member of the circle
  • Unlike the existing /admin/{emulated}/ endpoints which require user emulation per request, these endpoints use super sessions to operate directly as admin
  • Based on the SURF circlesadmin app which has been running in production

New endpoints

Method Endpoint Description
GET /manage/circles List all circles
GET /manage/circles/{circleId} Get circle details with members
POST /manage/circles Create circle (params: name, owner, desc, federated)
PUT /manage/circles/{circleId} Update circle name/description
DELETE /manage/circles/{circleId} Delete circle
GET /manage/circles/{circleId}/members List members
POST /manage/circles/{circleId}/members Add member
DELETE /manage/circles/{circleId}/members/{memberId} Remove member
PUT /manage/circles/{circleId}/members/{memberId}/level Set member level

Files added/changed

  • New: lib/Service/CirclesAdminService.php — business logic using CirclesManager super/occ sessions
  • New: lib/Controller/CircleApiController.php — circle CRUD endpoints (@AdminRequired)
  • New: lib/Controller/MemberApiController.php — member management endpoints (@AdminRequired)
  • Modified: appinfo/routes.php — 9 new OCS routes under /manage/

Test plan

  • GET /manage/circles — returns list of all circles
  • POST /manage/circles — create local circle with description
  • POST /manage/circles with federated=1 — create federated circle (config=0)
  • GET /manage/circles/{id} — returns circle detail with members array
  • PUT /manage/circles/{id} — update name and description
  • DELETE /manage/circles/{id} — delete circle
  • POST /manage/circles/{id}/members — add member to circle
  • GET /manage/circles/{id}/members — list all members
  • PUT /manage/circles/{id}/members/{id}/level — change member level
  • DELETE /manage/circles/{id}/members/{id} — remove member

All endpoints tested on Nextcloud 32 and Nextcloud 33 instances.

@artonge artonge requested a review from cristianscheid April 1, 2026 16:34
Adds /manage/ endpoints that allow admins to fully manage circles
(teams) without needing to be a member. Unlike the existing /admin/
endpoints which require user emulation, these endpoints use super
sessions to operate directly as admin.

New endpoints:
- GET/POST/PUT/DELETE /manage/circles — CRUD for circles
- GET/POST/DELETE /manage/circles/{id}/members — member management
- PUT /manage/circles/{id}/members/{id}/level — set member level

Based on the SURF circlesadmin app (sara-nl/nextcloud-circleadmin-api).

Signed-off-by: Sam428-png <s.d.ditmeijer@hva.nl>
@Sam428-png Sam428-png force-pushed the feat/admin-manage-api branch from 0ae8207 to eb830b3 Compare April 2, 2026 07:47
Copy link
Copy Markdown
Contributor

@cristianscheid cristianscheid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sam428-png thanks a lot for taking the time to implement this PR, really appreciated! I tested it locally and left a couple of suggestions inline. I also have one more to add: looking at how other controllers are named and structured, I think it would make sense to merge CircleApiController and MemberApiController into a single file called AdminApiController, and rename CirclesAdminService to AdminApiService for consistency. Thanks again!

Comment thread lib/Controller/CircleApiController.php Outdated
Comment thread lib/Service/AdminApiService.php
Comment thread lib/Service/CirclesAdminService.php Outdated
Comment thread lib/Service/CirclesAdminService.php Outdated
Comment thread lib/Service/CirclesAdminService.php Outdated
Comment thread lib/Service/CirclesAdminService.php Outdated
- Merge CircleApiController and MemberApiController into AdminApiController
- Rename CirclesAdminService to AdminApiService for consistency
- Fix desc → description parameter name in create endpoint
- Fix createCircle config logic: only set federated config when federated=true
- Simplify getUserTypeName/getLevelName using Member::$TYPE and Member::$DEF_LEVEL
- Update routes.php accordingly
Copy link
Copy Markdown
Author

@Sam428-png Sam428-png left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all feedback addressed!

@Sam428-png
Copy link
Copy Markdown
Author

Hi @cristianscheid, thanks again for the thorough review! While implementing your suggestion for createCircle, I noticed a potential issue: looking at CircleService::createCircle(), it sets CFG_PERSONAL = 2 by default — not 0 as observed in your tests. This means that when $federated = false, the config returned in the response (0) may not match what is actually stored in the database (2). Would it make more sense to re-fetch the circle after the update (like updateCircle() does) to return the actual persisted config value? Happy to adjust if you agree!

@cristianscheid
Copy link
Copy Markdown
Contributor

Hi @cristianscheid, thanks again for the thorough review! While implementing your suggestion for createCircle, I noticed a potential issue: looking at CircleService::createCircle(), it sets CFG_PERSONAL = 2 by default — not 0 as observed in your tests. This means that when $federated = false, the config returned in the response (0) may not match what is actually stored in the database (2). Would it make more sense to re-fetch the circle after the update (like updateCircle() does) to return the actual persisted config value? Happy to adjust if you agree!

Hey @Sam428-png, thank you for taking the time for this PR! Given the situation you just described, maybe we could do something like this then?

// instead of
if ($federated) {
	$updates['config'] = $federatedConfigValue;
}
// something like
$updates['config'] = $federated ? $federatedConfigValue : 0;

Does that make sense to you?

@github-actions
Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

Ensures the stored config matches the response payload by explicitly
setting config to 0 when federated=false, instead of relying on the
CFG_PERSONAL default.
@Sam428-png
Copy link
Copy Markdown
Author

Thanks for the feedback, and apologies for the late reply! This is now fixed in 9ff06b3config is always set explicitly (either CFG_ROOT + CFG_FEDERATED or 0), so the stored value matches the response payload.

Copy link
Copy Markdown
Contributor

@cristianscheid cristianscheid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sam428-png thanks for the adjustments! One more thing I caught after re-review: since $updates will now always be non-empty, there's no longer need for the if (!empty($updates)) check anymore.

Comment on lines +97 to +110
$updates = [];
$updates['config'] = $federated ? $federatedConfigValue : 0;
if ($description !== null && $description !== '') {
$updates['description'] = $description;
}
if (!empty($updates)) {
$qb = $this->db->getQueryBuilder();
$qb->update('circles_circle')
->where($qb->expr()->eq('unique_id', $qb->createNamedParameter($circleId)));
foreach ($updates as $column => $value) {
$qb->set($column, $qb->createNamedParameter($value));
}
$qb->executeStatement();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$updates = [];
$updates['config'] = $federated ? $federatedConfigValue : 0;
if ($description !== null && $description !== '') {
$updates['description'] = $description;
}
if (!empty($updates)) {
$qb = $this->db->getQueryBuilder();
$qb->update('circles_circle')
->where($qb->expr()->eq('unique_id', $qb->createNamedParameter($circleId)));
foreach ($updates as $column => $value) {
$qb->set($column, $qb->createNamedParameter($value));
}
$qb->executeStatement();
}
$updates = [
'config' => $federated ? $federatedConfigValue : 0,
];
if ($description !== null && $description !== '') {
$updates['description'] = $description;
}
$qb = $this->db->getQueryBuilder();
$qb->update('circles_circle')
->where($qb->expr()->eq('unique_id', $qb->createNamedParameter($circleId)));
foreach ($updates as $column => $value) {
$qb->set($column, $qb->createNamedParameter($value));
}
$qb->executeStatement();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants