Skip to content

Commit 4d0026d

Browse files
committed
Add ListInvitationsService for /auth/invitations
1 parent 34e2b2d commit 4d0026d

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package org.restheart.accounts;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import com.mongodb.client.MongoClient;
6+
import org.restheart.plugins.accounts.AccountsConfigData;
7+
import org.restheart.accounts.util.DbHelper;
8+
import org.restheart.accounts.util.Errors;
9+
import org.restheart.accounts.util.RequestOverrides;
10+
import org.restheart.exchange.JsonRequest;
11+
import org.restheart.exchange.JsonResponse;
12+
import org.restheart.plugins.Inject;
13+
import org.restheart.plugins.JsonService;
14+
import org.restheart.plugins.OnInit;
15+
import org.restheart.plugins.RegisterPlugin;
16+
import org.restheart.security.ACLRegistry;
17+
import org.restheart.utils.BsonUtils;
18+
import org.restheart.utils.HttpStatus;
19+
20+
/**
21+
* GET /auth/invitations
22+
*
23+
* <p>Returns all pending (non-expired) invitations for the caller's active team.
24+
* Owner/admin only — members cannot list invitations.
25+
*
26+
* <p>Response body example:
27+
* <pre>{@code
28+
* [
29+
* { "email": "bob@example.com", "role": "member", "isNewUser": true, "createdAt": "2026-01-01T00:00:00Z", "expiresAt": "2026-01-02T00:00:00Z" },
30+
* { "email": "carol@example.com", "role": "member", "isNewUser": false, "createdAt": "2026-01-01T00:00:00Z", "expiresAt": "2026-01-02T00:00:00Z" }
31+
* ]
32+
* }</pre>
33+
*
34+
* <p>The token field is intentionally excluded from the response (sensitive — one-shot secret).
35+
*
36+
* <p>This endpoint can be disabled via {@code accountsConfig.membership-endpoints-enabled: false}.
37+
*/
38+
@RegisterPlugin(
39+
name = "listInvitationsService",
40+
description = "GET /auth/invitations — list pending invitations for the caller's active team",
41+
defaultURI = "/auth/invitations",
42+
secure = true,
43+
enabledByDefault = false)
44+
public class ListInvitationsService implements JsonService {
45+
46+
@Inject("acl-registry")
47+
private ACLRegistry aclRegistry;
48+
49+
@Inject("accountsConfig")
50+
private AccountsConfigData conf;
51+
52+
@Inject("accountsService")
53+
private AccountsService accountsService;
54+
55+
@Inject("mclient")
56+
private MongoClient mclient;
57+
58+
@OnInit
59+
public void onInit() {
60+
if (conf.membershipEndpointsEnabled()) {
61+
aclRegistry.registerAllow(r -> r.getPath().equals("/auth/invitations") && (r.isGet() || r.isOptions()));
62+
}
63+
}
64+
65+
private DbHelper db(JsonRequest req) {
66+
return new DbHelper(mclient, RequestOverrides.db(req, conf));
67+
}
68+
69+
@Override
70+
public void handle(JsonRequest req, JsonResponse res) throws Exception {
71+
if (req.isOptions()) { handleOptions(req); return; }
72+
73+
if (!conf.membershipEndpointsEnabled()) {
74+
Errors.error(res, HttpStatus.SC_NOT_FOUND, "Endpoint not available");
75+
return;
76+
}
77+
78+
if (!req.isGet()) {
79+
res.setStatusCode(HttpStatus.SC_METHOD_NOT_ALLOWED);
80+
return;
81+
}
82+
83+
var account = req.getAuthenticatedAccount();
84+
var email = account.getPrincipal().getName();
85+
var membership = accountsService.getMembershipProvider(req);
86+
var active = membership.activeMembership(email);
87+
88+
if (active.isEmpty()) {
89+
Errors.error(res, HttpStatus.SC_BAD_REQUEST, "No active team");
90+
return;
91+
}
92+
93+
var invitations = db(req).listInvitationsByTeam(active.get().teamId());
94+
95+
var result = new JsonArray();
96+
for (var invite : invitations) {
97+
var obj = new JsonObject();
98+
obj.addProperty("email", invite.getString("email").getValue());
99+
obj.addProperty("role", invite.getString("role").getValue());
100+
obj.addProperty("isNewUser", invite.containsKey("isNewUser") && invite.getBoolean("isNewUser").getValue());
101+
if (invite.containsKey("createdAt")) {
102+
obj.addProperty("createdAt", java.time.Instant.ofEpochMilli(invite.getDateTime("createdAt").getValue()).toString());
103+
}
104+
if (invite.containsKey("expiresAt")) {
105+
obj.addProperty("expiresAt", java.time.Instant.ofEpochMilli(invite.getDateTime("expiresAt").getValue()).toString());
106+
}
107+
result.add(obj);
108+
}
109+
110+
res.setContent(result);
111+
res.setStatusCode(HttpStatus.SC_OK);
112+
}
113+
}

accounts/src/main/java/org/restheart/accounts/util/DbHelper.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,18 @@ public void deleteInvitations(String email, BsonValue teamId) {
503503
Filters.eq("email", email),
504504
Filters.eq("teamId", teamId)));
505505
}
506+
507+
/**
508+
* Lists all pending (non-expired) invitations for a specific team.
509+
* Does NOT include the token field (sensitive — one-shot secret).
510+
*/
511+
public List<BsonDocument> listInvitationsByTeam(BsonValue teamId) {
512+
var now = System.currentTimeMillis();
513+
return invitations()
514+
.find(Filters.and(
515+
Filters.eq("teamId", teamId),
516+
Filters.gt("expiresAt", now)))
517+
.projection(Filters.eq("token", 0))
518+
.into(new java.util.ArrayList<>());
519+
}
506520
}

0 commit comments

Comments
 (0)