Skip to content

Commit 048d04b

Browse files
committed
add support for rate limit tokens
1 parent e955abc commit 048d04b

14 files changed

Lines changed: 436 additions & 19 deletions

File tree

server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import org.springframework.stereotype.Service;
2727

2828
import java.time.LocalDateTime;
29-
import java.util.List;
3029
import java.util.UUID;
3130

3231
import static org.eclipse.openvsx.util.UrlUtil.createApiUrl;

server/src/main/java/org/eclipse/openvsx/admin/RateLimitAPI.java

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
import org.eclipse.openvsx.repositories.RepositoryService;
2020
import org.eclipse.openvsx.util.ErrorResultException;
2121
import org.eclipse.openvsx.util.LogService;
22+
import org.eclipse.openvsx.util.NotFoundException;
2223
import org.eclipse.openvsx.util.TimeUtil;
2324
import org.slf4j.Logger;
2425
import org.slf4j.LoggerFactory;
26+
import org.springframework.http.HttpStatus;
2527
import org.springframework.http.MediaType;
2628
import org.springframework.http.ResponseEntity;
2729
import org.springframework.web.bind.annotation.*;
30+
import org.springframework.web.server.ResponseStatusException;
2831

32+
import java.util.List;
2933
import java.util.Optional;
3034

3135

@@ -34,6 +38,8 @@
3438
public class RateLimitAPI {
3539
private final Logger logger = LoggerFactory.getLogger(RateLimitAPI.class);
3640

41+
private static final int TOKEN_DESCRIPTION_SIZE = 255;
42+
3743
private final RepositoryService repositories;
3844
private final AdminService admins;
3945
private final LogService logs;
@@ -59,9 +65,9 @@ public RateLimitAPI(
5965
produces = MediaType.APPLICATION_JSON_VALUE
6066
)
6167
public ResponseEntity<TierListJson> getTiers() {
62-
try {
63-
admins.checkAdminUser();
68+
admins.checkAdminUser();
6469

70+
try {
6571
var tiers = repositories.findAllTiers();
6672
var result = new TierListJson(tiers.stream().map(Tier::toJson).toList());
6773
return ResponseEntity.ok(result);
@@ -104,6 +110,8 @@ public ResponseEntity<TierJson> createTier(@RequestBody TierJson tier) {
104110
}
105111

106112
return ResponseEntity.ok(result);
113+
} catch (ErrorResultException exc) {
114+
return exc.toResponseEntity(TierJson.class);
107115
} catch (Exception exc) {
108116
logger.error("failed creating tier {}", tier.getName(), exc);
109117
return ResponseEntity.internalServerError().build();
@@ -144,6 +152,8 @@ public ResponseEntity<TierJson> updateTier(@PathVariable String name, @RequestBo
144152
}
145153

146154
return ResponseEntity.ok(result);
155+
} catch (ErrorResultException exc) {
156+
return exc.toResponseEntity(TierJson.class);
147157
} catch (Exception exc) {
148158
logger.error("failed updating tier {}", name, exc);
149159
return ResponseEntity.internalServerError().build();
@@ -178,6 +188,8 @@ public ResponseEntity<ResultJson> deleteTier(@PathVariable String name) {
178188
}
179189

180190
return ResponseEntity.ok(result);
191+
} catch (ErrorResultException exc) {
192+
return exc.toResponseEntity();
181193
} catch (Exception exc) {
182194
logger.error("failed deleting tier {}", name, exc);
183195
return ResponseEntity.internalServerError().build();
@@ -189,9 +201,9 @@ public ResponseEntity<ResultJson> deleteTier(@PathVariable String name) {
189201
produces = MediaType.APPLICATION_JSON_VALUE
190202
)
191203
public ResponseEntity<CustomerListJson> getCustomersForTier(@PathVariable String name) {
192-
try {
193-
admins.checkAdminUser();
204+
admins.checkAdminUser();
194205

206+
try {
195207
var tier = repositories.findTier(name);
196208
if (tier == null) {
197209
return ResponseEntity.notFound().build();
@@ -211,9 +223,9 @@ public ResponseEntity<CustomerListJson> getCustomersForTier(@PathVariable String
211223
produces = MediaType.APPLICATION_JSON_VALUE
212224
)
213225
public ResponseEntity<CustomerListJson> getCustomers() {
214-
try {
215-
admins.checkAdminUser();
226+
admins.checkAdminUser();
216227

228+
try {
217229
var customers = repositories.findAllCustomers();
218230
var result = new CustomerListJson(customers.stream().map(Customer::toJson).toList());
219231
return ResponseEntity.ok(result);
@@ -237,6 +249,8 @@ public ResponseEntity<CustomerJson> getCustomer(@PathVariable String name) {
237249
}
238250

239251
return ResponseEntity.ok(customer.toJson());
252+
} catch (ErrorResultException exc) {
253+
return exc.toResponseEntity(CustomerJson.class);
240254
} catch (Exception exc) {
241255
logger.error("failed retrieving customer {}", name, exc);
242256
return ResponseEntity.internalServerError().build();
@@ -280,6 +294,8 @@ public ResponseEntity<CustomerJson> createCustomer(@RequestBody CustomerJson cus
280294
}
281295

282296
return ResponseEntity.ok(result);
297+
} catch (ErrorResultException exc) {
298+
return exc.toResponseEntity(CustomerJson.class);
283299
} catch (Exception exc) {
284300
logger.error("failed creating customer {}", customerJson.getName(), exc);
285301
return ResponseEntity.internalServerError().build();
@@ -321,6 +337,8 @@ public ResponseEntity<CustomerJson> updateCustomer(@PathVariable String name, @R
321337
}
322338

323339
return ResponseEntity.ok(result);
340+
} catch (ErrorResultException exc) {
341+
return exc.toResponseEntity(CustomerJson.class);
324342
} catch (Exception exc) {
325343
logger.error("failed updating tier {}", name, exc);
326344
return ResponseEntity.internalServerError().build();
@@ -344,6 +362,8 @@ public ResponseEntity<CustomerMembershipListJson> getCustomerMembers(@PathVariab
344362
var membershipList = new CustomerMembershipListJson();
345363
membershipList.setCustomerMemberships(memberships.stream().map(CustomerMembership::toJson).toList());
346364
return ResponseEntity.ok(membershipList);
365+
} catch (ErrorResultException exc) {
366+
return exc.toResponseEntity(CustomerMembershipListJson.class);
347367
} catch (Exception exc) {
348368
logger.error("failed retrieving customer members {}", name, exc);
349369
return ResponseEntity.internalServerError().build();
@@ -419,6 +439,8 @@ public ResponseEntity<ResultJson> deleteCustomer(@PathVariable String name) {
419439
}
420440

421441
return ResponseEntity.ok(result);
442+
} catch (ErrorResultException exc) {
443+
return exc.toResponseEntity();
422444
} catch (Exception exc) {
423445
logger.error("failed deleting customer {}", name, exc);
424446
return ResponseEntity.internalServerError().build();
@@ -430,9 +452,9 @@ public ResponseEntity<ResultJson> deleteCustomer(@PathVariable String name) {
430452
produces = MediaType.APPLICATION_JSON_VALUE
431453
)
432454
public ResponseEntity<UsageStatsListJson> getUsageStats(@PathVariable String name, @RequestParam(required = false) String date) {
433-
try {
434-
admins.checkAdminUser();
455+
admins.checkAdminUser();
435456

457+
try {
436458
var customer = repositories.findCustomer(name);
437459
if (customer == null) {
438460
return ResponseEntity.notFound().build();
@@ -447,4 +469,83 @@ public ResponseEntity<UsageStatsListJson> getUsageStats(@PathVariable String nam
447469
return ResponseEntity.internalServerError().build();
448470
}
449471
}
472+
473+
@GetMapping(
474+
path = "/customers/{name}/tokens",
475+
produces = MediaType.APPLICATION_JSON_VALUE
476+
)
477+
public ResponseEntity<List<RateLimitTokenJson>> getRateLimitTokens(@PathVariable String name) {
478+
admins.checkAdminUser();
479+
480+
try {
481+
var customer = repositories.findCustomer(name);
482+
if (customer == null) {
483+
return ResponseEntity.notFound().build();
484+
}
485+
486+
repositories.findActiveRateLimitTokens(customer);
487+
var tokens = repositories.findActiveRateLimitTokens(customer)
488+
.map(RateLimitToken::toJson)
489+
.toList();
490+
491+
return ResponseEntity.ok(tokens);
492+
} catch (Exception exc) {
493+
logger.error("failed retrieving rate limit tokens", exc);
494+
return ResponseEntity.internalServerError().build();
495+
}
496+
}
497+
498+
@PostMapping(
499+
path = "/customers/{name}/tokens",
500+
produces = MediaType.APPLICATION_JSON_VALUE
501+
)
502+
public ResponseEntity<RateLimitTokenJson> createRateLimitToken(
503+
@PathVariable String name,
504+
@RequestParam(required = false) String description
505+
) {
506+
try {
507+
admins.checkAdminUser();
508+
509+
if (description != null && description.length() > TOKEN_DESCRIPTION_SIZE) {
510+
var json = RateLimitTokenJson.error("The description must not be longer than " + TOKEN_DESCRIPTION_SIZE + " characters.");
511+
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
512+
}
513+
514+
var customer = repositories.findCustomer(name);
515+
if (customer == null) {
516+
return ResponseEntity.notFound().build();
517+
}
518+
519+
return new ResponseEntity<>(customerService.createRateLimitToken(customer, description), HttpStatus.CREATED);
520+
} catch (ErrorResultException exc) {
521+
return exc.toResponseEntity(RateLimitTokenJson.class);
522+
} catch (Exception exc) {
523+
logger.error("failed creating rate limit token", exc);
524+
return ResponseEntity.internalServerError().build();
525+
}
526+
}
527+
528+
@DeleteMapping(
529+
path = "/customers/{name}/tokens/{id}",
530+
produces = MediaType.APPLICATION_JSON_VALUE
531+
)
532+
public ResponseEntity<ResultJson> deactivateRateLimitToken(@PathVariable String name, @PathVariable long id) {
533+
try {
534+
admins.checkAdminUser();
535+
536+
var customer = repositories.findCustomer(name);
537+
if (customer == null) {
538+
return ResponseEntity.notFound().build();
539+
}
540+
541+
return ResponseEntity.ok(customerService.deactivateRateLimitToken(customer, id));
542+
} catch (ErrorResultException exc) {
543+
return exc.toResponseEntity();
544+
} catch (NotFoundException exc) {
545+
return new ResponseEntity<>(ResultJson.error("Rate limit token does not exist."), HttpStatus.NOT_FOUND);
546+
} catch (Exception exc) {
547+
logger.error("failed deactivating rate limit token", exc);
548+
return ResponseEntity.internalServerError().build();
549+
}
550+
}
450551
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/******************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0.
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*****************************************************************************/
13+
package org.eclipse.openvsx.entities;
14+
15+
import jakarta.persistence.*;
16+
import org.eclipse.openvsx.json.RateLimitTokenJson;
17+
import org.eclipse.openvsx.util.TimeUtil;
18+
19+
import java.io.Serial;
20+
import java.io.Serializable;
21+
import java.time.LocalDateTime;
22+
import java.util.Objects;
23+
24+
@Entity
25+
@Table(name = "rate_limit_token")
26+
public class RateLimitToken implements Serializable {
27+
28+
@Serial
29+
private static final long serialVersionUID = 1L;
30+
31+
@Id
32+
@GeneratedValue(generator = "rateLimitTokenSeq")
33+
@SequenceGenerator(name = "rateLimitTokenSeq", sequenceName = "rate_limit_token_seq", allocationSize = 1)
34+
private long id;
35+
36+
@ManyToOne
37+
@JoinColumn(name = "customer")
38+
private Customer customer;
39+
40+
@Column(length = 64)
41+
private String value;
42+
43+
private boolean active;
44+
45+
private LocalDateTime createdTimestamp;
46+
47+
@Column(length = 2048)
48+
private String description;
49+
50+
/**
51+
* Convert to a JSON object.
52+
*/
53+
public RateLimitTokenJson toJson() {
54+
var json = new RateLimitTokenJson();
55+
json.setId(this.getId());
56+
// The value is not included: it is displayed only when the token is created
57+
if (this.getCreatedTimestamp() != null) {
58+
json.setCreatedTimestamp(TimeUtil.toUTCString(this.getCreatedTimestamp()));
59+
}
60+
json.setDescription(this.getDescription());
61+
return json;
62+
}
63+
64+
public long getId() {
65+
return id;
66+
}
67+
68+
public void setId(long id) {
69+
this.id = id;
70+
}
71+
72+
public Customer getCustomer() {
73+
return customer;
74+
}
75+
76+
public void setCustomer(Customer customer) {
77+
this.customer = customer;
78+
}
79+
80+
public String getValue() {
81+
return value;
82+
}
83+
84+
public void setValue(String value) {
85+
this.value = value;
86+
}
87+
88+
public boolean isActive() {
89+
return active;
90+
}
91+
92+
public void setActive(boolean active) {
93+
this.active = active;
94+
}
95+
96+
public LocalDateTime getCreatedTimestamp() {
97+
return createdTimestamp;
98+
}
99+
100+
public void setCreatedTimestamp(LocalDateTime timestamp) {
101+
this.createdTimestamp = timestamp;
102+
}
103+
104+
public String getDescription() {
105+
return description;
106+
}
107+
108+
public void setDescription(String description) {
109+
this.description = description;
110+
}
111+
112+
@Override
113+
public boolean equals(Object o) {
114+
if (this == o) return true;
115+
if (o == null || getClass() != o.getClass()) return false;
116+
RateLimitToken that = (RateLimitToken) o;
117+
return id == that.id
118+
&& active == that.active
119+
&& Objects.equals(customer, that.customer)
120+
&& Objects.equals(value, that.value)
121+
&& Objects.equals(createdTimestamp, that.createdTimestamp)
122+
&& Objects.equals(description, that.description);
123+
}
124+
125+
@Override
126+
public int hashCode() {
127+
return Objects.hash(id, customer, value, active, createdTimestamp, description);
128+
}
129+
}

0 commit comments

Comments
 (0)