Another rewrite of Q-App APIs, which removes the /apps/* redirects and instead calls the main APIs directly.

- All APIs are now served over the gateway and domain map, with the exception of /admin/*
- AdminResource moved to a "restricted" folder, so that it isn't served over the gateway/domainMap ports.
- This opens the door to websites/apps calling core APIs directly for certain read-only functions, as an alternative to using qortalRequest().
This commit is contained in:
CalDescent 2023-01-22 18:59:46 +00:00
parent 932a553b91
commit 8dffe1e3ac
8 changed files with 89 additions and 300 deletions

View File

@ -14,7 +14,6 @@ import java.security.SecureRandom;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import org.checkerframework.checker.units.qual.A;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
@ -53,7 +52,7 @@ public class ApiService {
private ApiService() {
this.config = new ResourceConfig();
this.config.packages("org.qortal.api.resource", "org.qortal.api.apps.resource");
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
this.config.register(OpenApiResource.class);
this.config.register(ApiDefinition.class);
this.config.register(AnnotationPostProcessor.class);

View File

@ -3,7 +3,6 @@ package org.qortal.api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.rewrite.handler.RewritePatternRule;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.InetAccessHandler;
@ -38,7 +37,7 @@ public class DomainMapService {
private DomainMapService() {
this.config = new ResourceConfig();
this.config.packages("org.qortal.api.domainmap.resource");
this.config.packages("org.qortal.api.resource", "org.qortal.api.domainmap.resource");
this.config.register(OpenApiResource.class);
this.config.register(ApiDefinition.class);
this.config.register(AnnotationPostProcessor.class);

View File

@ -37,7 +37,7 @@ public class GatewayService {
private GatewayService() {
this.config = new ResourceConfig();
this.config.packages("org.qortal.api.gateway.resource", "org.qortal.api.apps.resource");
this.config.packages("org.qortal.api.resource", "org.qortal.api.gateway.resource");
this.config.register(OpenApiResource.class);
this.config.register(ApiDefinition.class);
this.config.register(AnnotationPostProcessor.class);

View File

@ -1,257 +0,0 @@
package org.qortal.api.apps.resource;
import com.google.common.io.Resources;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.*;
import org.qortal.api.model.NameSummary;
import org.qortal.api.resource.*;
import org.qortal.arbitrary.misc.Service;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.data.account.AccountData;
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
import org.qortal.data.at.ATData;
import org.qortal.data.block.BlockData;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.group.GroupData;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Path("/apps")
@Tag(name = "Apps")
public class AppsResource {
@Context HttpServletRequest request;
@Context HttpServletResponse response;
@Context ServletContext context;
@GET
@Path("/q-apps.js")
@Hidden // For internal Q-App API use only
@Operation(
summary = "Javascript interface for Q-Apps",
responses = {
@ApiResponse(
description = "javascript",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String getQAppsJs() {
URL url = Resources.getResource("q-apps/q-apps.js");
try {
return Resources.toString(url, StandardCharsets.UTF_8);
} catch (IOException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
}
}
@GET
@Path("/q-apps-helper.js")
@Hidden // For testing only
public String getQAppsHelperJs() {
URL url = Resources.getResource("q-apps/q-apps-helper.js");
try {
return Resources.toString(url, StandardCharsets.UTF_8);
} catch (IOException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
}
}
@GET
@Path("/account")
@Hidden // For internal Q-App API use only
public AccountData getAccount(@QueryParam("address") String address) {
AddressesResource addressesResource = (AddressesResource) buildResource(AddressesResource.class, request, response, context);
return addressesResource.getAccountInfo(address);
}
@GET
@Path("/account/names")
@Hidden // For internal Q-App API use only
public List<NameSummary> getAccountNames(@QueryParam("address") String address) {
NamesResource namesResource = (NamesResource) buildResource(NamesResource.class, request, response, context);
return namesResource.getNamesByAddress(address, 0, 0 ,false);
}
@GET
@Path("/name")
@Hidden // For internal Q-App API use only
public NameData getName(@QueryParam("name") String name) {
NamesResource namesResource = (NamesResource) buildResource(NamesResource.class, request, response, context);
return namesResource.getName(name);
}
@GET
@Path("/chatmessages")
@Hidden // For internal Q-App API use only
public List<ChatMessage> searchChatMessages(@QueryParam("before") Long before, @QueryParam("after") Long after, @QueryParam("txGroupId") Integer txGroupId, @QueryParam("involving") List<String> involvingAddresses, @QueryParam("reference") String reference, @QueryParam("chatReference") String chatReference, @QueryParam("hasChatReference") Boolean hasChatReference, @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset, @QueryParam("reverse") Boolean reverse) {
ChatResource chatResource = (ChatResource) buildResource(ChatResource.class, request, response, context);
return chatResource.searchChat(before, after, txGroupId, involvingAddresses, reference, chatReference, hasChatReference, limit, offset, reverse);
}
@GET
@Path("/resources")
@Hidden // For internal Q-App API use only
public List<ArbitraryResourceInfo> getResources(@QueryParam("service") Service service, @QueryParam("identifier") String identifier, @Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource, @Parameter(description = "Filter names by list") @QueryParam("nameListFilter") String nameListFilter, @Parameter(description = "Include status") @QueryParam("includeStatus") Boolean includeStatus, @Parameter(description = "Include metadata") @QueryParam("includeMetadata") Boolean includeMetadata, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
ArbitraryResource arbitraryResource = (ArbitraryResource) buildResource(ArbitraryResource.class, request, response, context);
return arbitraryResource.getResources(service, identifier, defaultResource, limit, offset, reverse, nameListFilter, includeStatus, includeMetadata);
}
@GET
@Path("/resourcestatus")
@Hidden // For internal Q-App API use only
public ArbitraryResourceStatus getResourceStatus(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier) {
ArbitraryResource arbitraryResource = (ArbitraryResource) buildResource(ArbitraryResource.class, request, response, context);
ApiKey apiKey = ApiService.getInstance().getApiKey();
return arbitraryResource.getResourceStatus(apiKey.toString(), service, name, identifier, false);
}
@GET
@Path("/resource")
@Hidden // For internal Q-App API use only
public HttpServletResponse getResource(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier, @QueryParam("filepath") String filepath, @QueryParam("rebuild") boolean rebuild) {
ArbitraryResource arbitraryResource = (ArbitraryResource) buildResource(ArbitraryResource.class, request, response, context);
ApiKey apiKey = ApiService.getInstance().getApiKey();
return arbitraryResource.get(apiKey.toString(), service, name, identifier, filepath, rebuild, false, 5);
}
@GET
@Path("/groups")
@Hidden // For internal Q-App API use only
public List<GroupData> listGroups(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
GroupsResource groupsResource = (GroupsResource) buildResource(GroupsResource.class, request, response, context);
return groupsResource.getAllGroups(limit, offset, reverse);
}
@GET
@Path("/balance")
@Hidden // For internal Q-App API use only
public BigDecimal getBalance(@QueryParam("assetId") long assetId, @QueryParam("address") String address) {
AddressesResource addressesResource = (AddressesResource) buildResource(AddressesResource.class, request, response, context);
return addressesResource.getBalance(address, assetId);
}
@GET
@Path("/at")
@Hidden // For internal Q-App API use only
public ATData getAT(@QueryParam("atAddress") String atAddress) {
AtResource atResource = (AtResource) buildResource(AtResource.class, request, response, context);
return atResource.getByAddress(atAddress);
}
@GET
@Path("/atdata")
@Hidden // For internal Q-App API use only
public String getATData(@QueryParam("atAddress") String atAddress) {
AtResource atResource = (AtResource) buildResource(AtResource.class, request, response, context);
return Base58.encode(atResource.getDataByAddress(atAddress));
}
@GET
@Path("/ats")
@Hidden // For internal Q-App API use only
public List<ATData> listATs(@QueryParam("codeHash58") String codeHash58, @QueryParam("isExecutable") Boolean isExecutable, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
AtResource atResource = (AtResource) buildResource(AtResource.class, request, response, context);
return atResource.getByFunctionality(codeHash58, isExecutable, limit, offset, reverse);
}
@GET
@Path("/block")
@Hidden // For internal Q-App API use only
public BlockData fetchBlockByHeight(@QueryParam("signature") String signature58, @QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
BlocksResource blocksResource = (BlocksResource) buildResource(BlocksResource.class, request, response, context);
return blocksResource.getBlock(signature58, includeOnlineSignatures);
}
@GET
@Path("/block/byheight")
@Hidden // For internal Q-App API use only
public BlockData fetchBlockByHeight(@QueryParam("height") int height, @QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
BlocksResource blocksResource = (BlocksResource) buildResource(BlocksResource.class, request, response, context);
return blocksResource.getByHeight(height, includeOnlineSignatures);
}
@GET
@Path("/block/range")
@Hidden // For internal Q-App API use only
public List<BlockData> getBlockRange(@QueryParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse, @QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
BlocksResource blocksResource = (BlocksResource) buildResource(BlocksResource.class, request, response, context);
return blocksResource.getBlockRange(height, count, reverse, includeOnlineSignatures);
}
@GET
@Path("/transactions/search")
@Hidden // For internal Q-App API use only
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit, @QueryParam("txGroupId") Integer txGroupId, @QueryParam("txType") List<Transaction.TransactionType> txTypes, @QueryParam("address") String address, @Parameter() @QueryParam("confirmationStatus") TransactionsResource.ConfirmationStatus confirmationStatus, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
TransactionsResource transactionsResource = (TransactionsResource) buildResource(TransactionsResource.class, request, response, context);
return transactionsResource.searchTransactions(startBlock, blockLimit, txGroupId, txTypes, address, confirmationStatus, limit, offset, reverse);
}
@GET
@Path("/price")
@Hidden // For internal Q-App API use only
public long getPrice(@QueryParam("blockchain") SupportedBlockchain foreignBlockchain, @QueryParam("maxtrades") Integer maxtrades, @QueryParam("inverse") Boolean inverse) {
CrossChainResource crossChainResource = (CrossChainResource) buildResource(CrossChainResource.class, request, response, context);
return crossChainResource.getTradePriceEstimate(foreignBlockchain, maxtrades, inverse);
}
public static Object buildResource(Class<?> resourceClass, HttpServletRequest request, HttpServletResponse response, ServletContext context) {
try {
Object resource = resourceClass.getDeclaredConstructor().newInstance();
Field requestField = resourceClass.getDeclaredField("request");
requestField.setAccessible(true);
requestField.set(resource, request);
try {
Field responseField = resourceClass.getDeclaredField("response");
responseField.setAccessible(true);
responseField.set(resource, response);
} catch (NoSuchFieldException e) {
// Ignore
}
try {
Field contextField = resourceClass.getDeclaredField("context");
contextField.setAccessible(true);
contextField.set(resource, context);
} catch (NoSuchFieldException e) {
// Ignore
}
return resource;
} catch (Exception e) {
throw new RuntimeException("Failed to build API resource " + resourceClass.getName() + ": " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,57 @@
package org.qortal.api.resource;
import com.google.common.io.Resources;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.*;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@Path("/apps")
@Tag(name = "Apps")
public class AppsResource {
@Context HttpServletRequest request;
@Context HttpServletResponse response;
@Context ServletContext context;
@GET
@Path("/q-apps.js")
@Hidden // For internal Q-App API use only
@Operation(
summary = "Javascript interface for Q-Apps",
responses = {
@ApiResponse(
description = "javascript",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String getQAppsJs() {
URL url = Resources.getResource("q-apps/q-apps.js");
try {
return Resources.toString(url, StandardCharsets.UTF_8);
} catch (IOException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
}
}
}

View File

@ -1,4 +1,4 @@
package org.qortal.api.resource;
package org.qortal.api.restricted.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;

View File

@ -60,25 +60,25 @@ window.addEventListener("message", (event) => {
switch (data.action) {
case "GET_ACCOUNT_DATA":
response = httpGet("/apps/account?address=" + data.address);
response = httpGet("/addresses/" + data.address);
break;
case "GET_ACCOUNT_NAMES":
response = httpGet("/apps/account/names?address=" + data.address);
response = httpGet("/names/address/" + data.address);
break;
case "GET_NAME_DATA":
response = httpGet("/apps/name?name=" + data.name);
response = httpGet("/names/" + data.name);
break;
case "SEARCH_QDN_RESOURCES":
url = "/apps/resources?";
url = "/arbitrary/resources?";
if (data.service != null) url = url.concat("&service=" + data.service);
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
if (data.default != null) url = url.concat("&default=" + data.default);
if (data.nameListFilter != null) url = url.concat("&nameListFilter=" + data.nameListFilter);
if (data.includeStatus != null) url = url.concat("&includeStatus=" + new Boolean(data.includeStatus).toString());
if (data.includeMetadata != null) url = url.concat("&includeMetadata=" + new Boolean(data.includeMetadata).toString());
if (data.nameListFilter != null) url = url.concat("&namefilter=" + data.nameListFilter);
if (data.includeStatus != null) url = url.concat("&includestatus=" + new Boolean(data.includeStatus).toString());
if (data.includeMetadata != null) url = url.concat("&includemetadata=" + new Boolean(data.includeMetadata).toString());
if (data.limit != null) url = url.concat("&limit=" + data.limit);
if (data.offset != null) url = url.concat("&offset=" + data.offset);
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
@ -86,32 +86,29 @@ window.addEventListener("message", (event) => {
break;
case "FETCH_QDN_RESOURCE":
url = "/apps/resource?";
if (data.service != null) url = url.concat("&service=" + data.service);
if (data.name != null) url = url.concat("&name=" + data.name);
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
url = "/arbitrary/" + data.service + "/" + data.name;
if (data.identifier != null) url = url.concat("/" + data.identifier);
url = url.concat("?");
if (data.filepath != null) url = url.concat("&filepath=" + data.filepath);
if (data.rebuild != null) url = url.concat("&rebuild=" + new Boolean(data.rebuild).toString())
response = httpGet(url);
break;
case "GET_QDN_RESOURCE_STATUS":
url = "/apps/resourcestatus?";
if (data.service != null) url = url.concat("&service=" + data.service);
if (data.name != null) url = url.concat("&name=" + data.name);
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
url = "/arbitrary/resource/status/" + data.service + "/" + data.name;
if (data.identifier != null) url = url.concat("/" + data.identifier);
response = httpGet(url);
break;
case "SEARCH_CHAT_MESSAGES":
url = "/apps/chatmessages?";
url = "/chat/messages?";
if (data.before != null) url = url.concat("&before=" + data.before);
if (data.after != null) url = url.concat("&after=" + data.after);
if (data.txGroupId != null) url = url.concat("&txGroupId=" + data.txGroupId);
if (data.involving != null) data.involving.forEach((x, i) => url = url.concat("&involving=" + x));
if (data.reference != null) url = url.concat("&reference=" + data.reference);
if (data.chatReference != null) url = url.concat("&chatReference=" + data.chatReference);
if (data.hasChatReference != null) url = url.concat("&hasChatReference=" + new Boolean(data.hasChatReference).toString());
if (data.chatReference != null) url = url.concat("&chatreference=" + data.chatReference);
if (data.hasChatReference != null) url = url.concat("&haschatreference=" + new Boolean(data.hasChatReference).toString());
if (data.limit != null) url = url.concat("&limit=" + data.limit);
if (data.offset != null) url = url.concat("&offset=" + data.offset);
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
@ -119,7 +116,7 @@ window.addEventListener("message", (event) => {
break;
case "LIST_GROUPS":
url = "/apps/groups?";
url = "/groups?";
if (data.limit != null) url = url.concat("&limit=" + data.limit);
if (data.offset != null) url = url.concat("&offset=" + data.offset);
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
@ -127,27 +124,23 @@ window.addEventListener("message", (event) => {
break;
case "GET_BALANCE":
url = "/apps/balance?";
url = "/addresses/balance/" + data.address;
if (data.assetId != null) url = url.concat("&assetId=" + data.assetId);
if (data.address != null) url = url.concat("&address=" + data.address);
response = httpGet(url);
break;
case "GET_AT":
url = "/apps/at?";
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
url = "/at" + data.atAddress;
response = httpGet(url);
break;
case "GET_AT_DATA":
url = "/apps/atdata?";
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
url = "/at/" + data.atAddress + "/data";
response = httpGet(url);
break;
case "LIST_ATS":
url = "/apps/ats?";
if (data.codeHash58 != null) url = url.concat("&codeHash58=" + data.codeHash58);
url = "/at/byfunction/" + data.codeHash58 + "?";
if (data.isExecutable != null) url = url.concat("&isExecutable=" + data.isExecutable);
if (data.limit != null) url = url.concat("&limit=" + data.limit);
if (data.offset != null) url = url.concat("&offset=" + data.offset);
@ -157,20 +150,18 @@ window.addEventListener("message", (event) => {
case "FETCH_BLOCK":
if (data.signature != null) {
url = "/apps/block?";
url = url.concat("&signature=" + data.signature);
url = "/blocks/" + data.signature;
}
else if (data.height != null) {
url = "/apps/block/byheight?";
url = url.concat("&height=" + data.height);
url = "/blocks/byheight/" + data.height;
}
url = url.concat("?");
if (data.includeOnlineSignatures != null) url = url.concat("&includeOnlineSignatures=" + data.includeOnlineSignatures);
response = httpGet(url);
break;
case "FETCH_BLOCK_RANGE":
url = "/apps/block/range?";
if (data.height != null) url = url.concat("&height=" + data.height);
url = "/blocks/range/" + data.height + "?";
if (data.count != null) url = url.concat("&count=" + data.count);
if (data.reverse != null) url = url.concat("&reverse=" + data.reverse);
if (data.includeOnlineSignatures != null) url = url.concat("&includeOnlineSignatures=" + data.includeOnlineSignatures);
@ -178,11 +169,12 @@ window.addEventListener("message", (event) => {
break;
case "SEARCH_TRANSACTIONS":
url = "/apps/transactions/search?";
url = "/transactions/search?";
if (data.startBlock != null) url = url.concat("&startBlock=" + data.startBlock);
if (data.blockLimit != null) url = url.concat("&blockLimit=" + data.blockLimit);
if (data.txGroupId != null) url = url.concat("&txGroupId=" + data.txGroupId);
if (data.txType != null) data.txType.forEach((x, i) => url = url.concat("&txType=" + x));
if (data.address != null) url = url.concat("&address=" + data.address);
if (data.confirmationStatus != null) url = url.concat("&confirmationStatus=" + data.confirmationStatus);
if (data.limit != null) url = url.concat("&limit=" + data.limit);
if (data.offset != null) url = url.concat("&offset=" + data.offset);
@ -191,8 +183,7 @@ window.addEventListener("message", (event) => {
break;
case "GET_PRICE":
url = "/apps/price?";
if (data.blockchain != null) url = url.concat("&blockchain=" + data.blockchain);
url = "/crosschain/price/" + data.blockchain + "?";
if (data.maxtrades != null) url = url.concat("&maxtrades=" + data.maxtrades);
if (data.inverse != null) url = url.concat("&inverse=" + data.inverse);
response = httpGet(url);

View File

@ -5,7 +5,7 @@ import static org.junit.Assert.*;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.Before;
import org.junit.Test;
import org.qortal.api.resource.AdminResource;
import org.qortal.api.restricted.resource.AdminResource;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
import org.qortal.test.common.ApiCommon;