Refactored to use @PathMapping everywhere, and add rate-limiter
Build and Deploy API / build-and-deploy (push) Successful in 1m45s
Details
Build and Deploy API / build-and-deploy (push) Successful in 1m45s
Details
This commit is contained in:
parent
7ef9d63de7
commit
a142a847da
|
|
@ -11,6 +11,7 @@ import std.datetime;
|
||||||
|
|
||||||
import profile.service;
|
import profile.service;
|
||||||
import profile.data;
|
import profile.data;
|
||||||
|
import profile.api : PROFILE_PATH;
|
||||||
import account.model;
|
import account.model;
|
||||||
import account.service;
|
import account.service;
|
||||||
import account.dto;
|
import account.dto;
|
||||||
|
|
@ -21,6 +22,9 @@ import account.data;
|
||||||
import attachment.data;
|
import attachment.data;
|
||||||
import attachment.dto;
|
import attachment.dto;
|
||||||
|
|
||||||
|
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/accounts")
|
||||||
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import std.algorithm;
|
import std.algorithm;
|
||||||
import std.array;
|
import std.array;
|
||||||
|
|
@ -30,6 +34,7 @@ void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse res
|
||||||
writeJsonBody(response, accounts);
|
writeJsonBody(response, accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH)
|
||||||
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
|
|
@ -38,6 +43,7 @@ void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse resp
|
||||||
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/accounts")
|
||||||
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
||||||
|
|
@ -54,6 +60,7 @@ void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.PUT, ACCOUNT_PATH)
|
||||||
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request);
|
||||||
|
|
@ -73,12 +80,14 @@ void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id)));
|
writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH)
|
||||||
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
ds.getAccountRepository().deleteById(accountId);
|
ds.getAccountRepository().deleteById(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/balance")
|
||||||
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
|
|
@ -96,6 +105,7 @@ void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpRespon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/history")
|
||||||
void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamOrThrow!ulong("accountId");
|
ulong accountId = request.getPathParamOrThrow!ulong("accountId");
|
||||||
PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]));
|
PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]));
|
||||||
|
|
@ -132,6 +142,7 @@ private void writeHistoryResponse(ref ServerHttpResponse response, in Page!Accou
|
||||||
response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON);
|
response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/account-balances")
|
||||||
void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
auto balances = getTotalBalanceForAllAccounts(ds);
|
auto balances = getTotalBalanceForAllAccounts(ds);
|
||||||
|
|
@ -142,6 +153,7 @@ void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpRespons
|
||||||
|
|
||||||
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]);
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records")
|
||||||
void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
|
|
@ -152,6 +164,7 @@ void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, page);
|
writeJsonBody(response, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong")
|
||||||
void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
||||||
|
|
@ -162,6 +175,7 @@ void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, AccountValueRecordResponse.of(record, attachmentRepo));
|
writeJsonBody(response, AccountValueRecordResponse.of(record, attachmentRepo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records")
|
||||||
void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -198,6 +212,7 @@ void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpRespon
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong")
|
||||||
void handleDeleteValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||||
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
module analytics.api;
|
module analytics.api;
|
||||||
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
|
import handy_http_handlers.path_handler : PathMapping;
|
||||||
|
|
||||||
import profile.data;
|
import profile.data;
|
||||||
import profile.service;
|
import profile.service;
|
||||||
|
import profile.api : PROFILE_PATH;
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/analytics/balance-time-series")
|
||||||
void handleGetBalanceTimeSeries(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetBalanceTimeSeries(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
serveJsonFromProperty(response, ds, "analytics.balanceTimeSeries");
|
serveJsonFromProperty(response, ds, "analytics.balanceTimeSeries");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/analytics/category-spend-time-series")
|
||||||
void handleGetCategorySpendTimeSeries(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetCategorySpendTimeSeries(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto ds = getProfileDataSource(request);
|
auto ds = getProfileDataSource(request);
|
||||||
serveJsonFromProperty(response, ds, "analytics.categorySpendTimeSeries");
|
serveJsonFromProperty(response, ds, "analytics.categorySpendTimeSeries");
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@ import handy_http_handlers.path_handler;
|
||||||
import handy_http_handlers.filtered_handler;
|
import handy_http_handlers.filtered_handler;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
/// The base path to all API endpoints.
|
|
||||||
private const API_PATH = "/api";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the Finnow API mapping with a main PathHandler.
|
* Defines the Finnow API mapping with a main PathHandler.
|
||||||
* Params:
|
* Params:
|
||||||
|
|
@ -15,92 +12,41 @@ private const API_PATH = "/api";
|
||||||
* Returns: The handler to plug into an HttpServer.
|
* Returns: The handler to plug into an HttpServer.
|
||||||
*/
|
*/
|
||||||
HttpRequestHandler mapApiHandlers(string webOrigin) {
|
HttpRequestHandler mapApiHandlers(string webOrigin) {
|
||||||
PathHandler h = new PathHandler();
|
PathHandler publicHandler = new PathHandler();
|
||||||
|
PathHandler authenticatedHandler = new PathHandler();
|
||||||
|
|
||||||
// Generic, public endpoints:
|
// Public endpoints:
|
||||||
h.map(HttpMethod.GET, "/status", &getStatus);
|
publicHandler.addMapping(HttpMethod.GET, "/api/status", HttpRequestHandler.of(&getStatus));
|
||||||
h.map(HttpMethod.OPTIONS, "/**", &getOptions);
|
publicHandler.addMapping(HttpMethod.OPTIONS, "/**", HttpRequestHandler.of(&getOptions));
|
||||||
|
// Note: the download endpoint is public! We authenticate via token in query params instead of header here.
|
||||||
|
import attachment.api;
|
||||||
|
publicHandler.registerHandlers!(attachment.api);
|
||||||
|
import auth.api_public;
|
||||||
|
publicHandler.registerHandlers!(auth.api_public);
|
||||||
|
|
||||||
// Dev endpoint for sample data: REMOVE BEFORE DEPLOYING!!!
|
// Dev endpoint for sample data: REMOVE BEFORE DEPLOYING!!!
|
||||||
// h.map(HttpMethod.POST, "/sample-data", &sampleDataEndpoint);
|
// h.map(HttpMethod.POST, "/sample-data", &sampleDataEndpoint);
|
||||||
|
|
||||||
// Auth endpoints:
|
|
||||||
import auth.api;
|
|
||||||
import auth.api_public;
|
|
||||||
h.registerHandlers!(auth.api_public);
|
|
||||||
|
|
||||||
// Authenticated endpoints:
|
// Authenticated endpoints:
|
||||||
PathHandler a = new PathHandler();
|
import auth.api;
|
||||||
a.registerHandlers!(auth.api);
|
authenticatedHandler.registerHandlers!(auth.api);
|
||||||
|
|
||||||
import profile.api;
|
import profile.api;
|
||||||
a.map(HttpMethod.GET, "/profiles", &handleGetProfiles);
|
authenticatedHandler.registerHandlers!(profile.api);
|
||||||
a.map(HttpMethod.POST, "/profiles", &handleCreateNewProfile);
|
|
||||||
/// URL path to a specific profile, with the :profile path parameter.
|
|
||||||
const PROFILE_PATH = "/profiles/:profile";
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH, &handleGetProfile);
|
|
||||||
a.map(HttpMethod.DELETE, PROFILE_PATH, &handleDeleteProfile);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/properties", &handleGetProperties);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/download", &handleDownloadProfile);
|
|
||||||
import attachment.api;
|
|
||||||
// Note: the download endpoint is public! We authenticate via token in query params instead of header here.
|
|
||||||
h.map(HttpMethod.GET, PROFILE_PATH ~ "/attachments/:attachmentId/download", &handleDownloadAttachment);
|
|
||||||
|
|
||||||
// Account endpoints:
|
|
||||||
import account.api;
|
import account.api;
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
authenticatedHandler.registerHandlers!(account.api);
|
||||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/account-balances", &handleGetTotalBalances);
|
|
||||||
const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
|
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH, &handleGetAccount);
|
|
||||||
a.map(HttpMethod.PUT, ACCOUNT_PATH, &handleUpdateAccount);
|
|
||||||
a.map(HttpMethod.DELETE, ACCOUNT_PATH, &handleDeleteAccount);
|
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/balance", &handleGetAccountBalance);
|
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/history", &handleGetAccountHistory);
|
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records", &handleGetValueRecords);
|
|
||||||
a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleGetValueRecord);
|
|
||||||
a.map(HttpMethod.POST, ACCOUNT_PATH ~ "/value-records", &handleCreateValueRecord);
|
|
||||||
a.map(HttpMethod.DELETE, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleDeleteValueRecord);
|
|
||||||
|
|
||||||
import transaction.api;
|
import transaction.api;
|
||||||
// Transaction vendor endpoints:
|
authenticatedHandler.registerHandlers!(transaction.api);
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/vendors", &handleGetVendors);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleGetVendor);
|
|
||||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/vendors", &handleCreateVendor);
|
|
||||||
a.map(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleUpdateVendor);
|
|
||||||
a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleDeleteVendor);
|
|
||||||
// Transaction category endpoints:
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/categories", &handleGetCategories);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleGetCategory);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong/children", &handleGetChildCategories);
|
|
||||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/categories", &handleCreateCategory);
|
|
||||||
a.map(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleUpdateCategory);
|
|
||||||
a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleDeleteCategory);
|
|
||||||
// Transaction endpoints:
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions", &handleGetTransactions);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions/search", &handleSearchTransactions);
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleGetTransaction);
|
|
||||||
a.map(HttpMethod.POST, PROFILE_PATH ~ "/transactions", &handleAddTransaction);
|
|
||||||
a.map(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleUpdateTransaction);
|
|
||||||
a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleDeleteTransaction);
|
|
||||||
|
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags", &handleGetAllTags);
|
|
||||||
|
|
||||||
// Analytics endpoints:
|
|
||||||
import analytics.api;
|
import analytics.api;
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/analytics/balance-time-series", &handleGetBalanceTimeSeries);
|
authenticatedHandler.registerHandlers!(analytics.api);
|
||||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/analytics/category-spend-time-series", &handleGetCategorySpendTimeSeries);
|
|
||||||
|
|
||||||
import data_api;
|
import data_api;
|
||||||
// Various other data endpoints:
|
authenticatedHandler.registerHandlers!(data_api);
|
||||||
a.map(HttpMethod.GET, "/currencies", &handleGetCurrencies);
|
|
||||||
|
|
||||||
// Protect all authenticated paths with a filter.
|
// Protect all authenticated paths with a filter.
|
||||||
import auth.service : AuthenticationFilter;
|
import auth.service : AuthenticationFilter;
|
||||||
HttpRequestFilter authenticationFilter = new AuthenticationFilter();
|
HttpRequestFilter authenticationFilter = new AuthenticationFilter();
|
||||||
h.addMapping(API_PATH ~ "/**", new FilteredHandler(
|
publicHandler.addMapping("/api/**", new FilteredHandler(
|
||||||
[authenticationFilter],
|
[authenticationFilter],
|
||||||
a
|
authenticatedHandler
|
||||||
));
|
));
|
||||||
|
|
||||||
// Build the main handler into a filter chain:
|
// Build the main handler into a filter chain:
|
||||||
|
|
@ -108,9 +54,10 @@ HttpRequestHandler mapApiHandlers(string webOrigin) {
|
||||||
[
|
[
|
||||||
cast(HttpRequestFilter) new CorsFilter(webOrigin),
|
cast(HttpRequestFilter) new CorsFilter(webOrigin),
|
||||||
cast(HttpRequestFilter) new ContentLengthFilter(),
|
cast(HttpRequestFilter) new ContentLengthFilter(),
|
||||||
|
cast(HttpRequestFilter) new TokenBucketRateLimitingFilter(10, 50),
|
||||||
cast(HttpRequestFilter) new ExceptionHandlingFilter()
|
cast(HttpRequestFilter) new ExceptionHandlingFilter()
|
||||||
],
|
],
|
||||||
h
|
publicHandler
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,15 +84,6 @@ private void sampleDataEndpoint(ref ServerHttpRequest request, ref ServerHttpRes
|
||||||
info("Started new thread to generate sample data.");
|
info("Started new thread to generate sample data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void map(
|
|
||||||
PathHandler handler,
|
|
||||||
HttpMethod method,
|
|
||||||
string subPath,
|
|
||||||
void function(ref ServerHttpRequest, ref ServerHttpResponse) fn
|
|
||||||
) {
|
|
||||||
handler.addMapping(method, API_PATH ~ subPath, HttpRequestHandler.of(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter that adds CORS response headers.
|
* A filter that adds CORS response headers.
|
||||||
*/
|
*/
|
||||||
|
|
@ -216,3 +154,90 @@ private class ExceptionHandlingFilter : HttpRequestFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter that uses a shared token bucket to limit clients' requests. Each
|
||||||
|
* client's IP address is used as the identifier, and each client is given a
|
||||||
|
* maximum of N requests to make, and a rate at which that limit replenishes
|
||||||
|
* over time.
|
||||||
|
*/
|
||||||
|
private class TokenBucketRateLimitingFilter : HttpRequestFilter {
|
||||||
|
import std.datetime;
|
||||||
|
import std.math : floor;
|
||||||
|
import std.algorithm : min;
|
||||||
|
|
||||||
|
private static struct TokenBucket {
|
||||||
|
uint tokens;
|
||||||
|
SysTime lastRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenBucket[string] tokenBuckets;
|
||||||
|
const uint tokensPerSecond;
|
||||||
|
const uint maxTokens;
|
||||||
|
|
||||||
|
this(uint tokensPerSecond, uint maxTokens) {
|
||||||
|
this.tokensPerSecond = tokensPerSecond;
|
||||||
|
this.maxTokens = maxTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
|
||||||
|
string clientAddr = getClientId(request);
|
||||||
|
bool shouldBlockRequest = false;
|
||||||
|
synchronized {
|
||||||
|
const now = Clock.currTime();
|
||||||
|
TokenBucket* bucket = getOrCreateBucket(clientAddr, now);
|
||||||
|
incrementTokensForElapsedTime(bucket, now);
|
||||||
|
if (bucket.tokens < 1) {
|
||||||
|
shouldBlockRequest = true;
|
||||||
|
} else {
|
||||||
|
bucket.tokens--;
|
||||||
|
}
|
||||||
|
bucket.lastRequest = now;
|
||||||
|
clearOldBuckets();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldBlockRequest) {
|
||||||
|
infoF!"Rate-limiting client %s because they have made too many requests."(clientAddr);
|
||||||
|
response.status = HttpStatus.TOO_MANY_REQUESTS;
|
||||||
|
response.writeBodyString("You have made too many requests to the API.");
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getClientId(in ServerHttpRequest req) {
|
||||||
|
import handy_http_transport.helpers : indexOf;
|
||||||
|
string clientAddr = req.clientAddress.toString();
|
||||||
|
auto portIdx = indexOf(clientAddr, ':');
|
||||||
|
if (portIdx != -1) {
|
||||||
|
clientAddr = clientAddr[0..portIdx];
|
||||||
|
}
|
||||||
|
return clientAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenBucket* getOrCreateBucket(string clientAddr, SysTime now) {
|
||||||
|
TokenBucket* bucket = clientAddr in tokenBuckets;
|
||||||
|
if (bucket is null) {
|
||||||
|
tokenBuckets[clientAddr] = TokenBucket(maxTokens, now);
|
||||||
|
bucket = clientAddr in tokenBuckets;
|
||||||
|
}
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementTokensForElapsedTime(TokenBucket* bucket, SysTime now) {
|
||||||
|
Duration timeSinceLastRequest = now - bucket.lastRequest;
|
||||||
|
const tokensAddedSinceLastRequest = floor((timeSinceLastRequest.total!"msecs") * (tokensPerSecond / 1000.0));
|
||||||
|
bucket.tokens = cast(uint) min(bucket.tokens + tokensAddedSinceLastRequest, maxTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearOldBuckets() {
|
||||||
|
const Duration fillTime = seconds(maxTokens * tokensPerSecond);
|
||||||
|
foreach (id; tokenBuckets.byKey()) {
|
||||||
|
TokenBucket bucket = tokenBuckets[id];
|
||||||
|
const Duration timeSinceLastRequest = Clock.currTime() - bucket.lastRequest;
|
||||||
|
if (timeSinceLastRequest > fillTime) {
|
||||||
|
tokenBuckets.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ void main() {
|
||||||
|
|
||||||
void configureSlf4d(in AppConfig config) {
|
void configureSlf4d(in AppConfig config) {
|
||||||
Level logLevel = getConfiguredLoggingLevel(config);
|
Level logLevel = getConfiguredLoggingLevel(config);
|
||||||
logLevel = Levels.DEBUG;
|
// logLevel = Levels.DEBUG;
|
||||||
auto provider = new DefaultProvider(logLevel);
|
auto provider = new DefaultProvider(logLevel);
|
||||||
configureLoggingProvider(provider);
|
configureLoggingProvider(provider);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ module attachment.api;
|
||||||
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import handy_http_data.json;
|
import handy_http_data.json;
|
||||||
|
import handy_http_handlers.path_handler : PathMapping;
|
||||||
import std.conv;
|
import std.conv;
|
||||||
|
|
||||||
import profile.data;
|
import profile.data;
|
||||||
|
|
@ -22,6 +23,7 @@ import attachment.model;
|
||||||
* request = The HTTP request.
|
* request = The HTTP request.
|
||||||
* response = The HTTP response.
|
* response = The HTTP response.
|
||||||
*/
|
*/
|
||||||
|
@PathMapping(HttpMethod.GET, "/api/profiles/:profile/attachments/:attachmentId/download")
|
||||||
void handleDownloadAttachment(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDownloadAttachment(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
Optional!AuthContext authCtx = extractAuthContextFromQueryParam(request, response);
|
Optional!AuthContext authCtx = extractAuthContextFromQueryParam(request, response);
|
||||||
if (authCtx.isNull) return;
|
if (authCtx.isNull) return;
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import auth.data;
|
||||||
import auth.service;
|
import auth.service;
|
||||||
import auth.data_impl_fs;
|
import auth.data_impl_fs;
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, "/me")
|
@PathMapping(HttpMethod.GET, "/api/me")
|
||||||
void getMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void getMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
AuthContext auth = getAuthContext(request);
|
AuthContext auth = getAuthContext(request);
|
||||||
response.writeBodyString(auth.user.username);
|
response.writeBodyString(auth.user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.DELETE, "/me")
|
@PathMapping(HttpMethod.DELETE, "/api/me")
|
||||||
void deleteMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void deleteMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
AuthContext auth = getAuthContext(request);
|
AuthContext auth = getAuthContext(request);
|
||||||
UserRepository userRepo = new FileSystemUserRepository();
|
UserRepository userRepo = new FileSystemUserRepository();
|
||||||
|
|
@ -25,7 +25,7 @@ void deleteMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response
|
||||||
infoF!"Deleted user: %s"(auth.user.username);
|
infoF!"Deleted user: %s"(auth.user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, "/me/token")
|
@PathMapping(HttpMethod.GET, "/api/me/token")
|
||||||
void getNewToken(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void getNewToken(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
AuthContext auth = getAuthContext(request);
|
AuthContext auth = getAuthContext(request);
|
||||||
string token = generateTokenForUser(auth.user);
|
string token = generateTokenForUser(auth.user);
|
||||||
|
|
@ -38,7 +38,7 @@ struct PasswordChangeRequest {
|
||||||
string newPassword;
|
string newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, "/me/password")
|
@PathMapping(HttpMethod.POST, "/api/me/password")
|
||||||
void changeMyPassword(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void changeMyPassword(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
AuthContext auth = getAuthContext(request);
|
AuthContext auth = getAuthContext(request);
|
||||||
PasswordChangeRequest data = readJsonBodyAs!PasswordChangeRequest(request);
|
PasswordChangeRequest data = readJsonBodyAs!PasswordChangeRequest(request);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import auth.data;
|
||||||
import auth.service;
|
import auth.service;
|
||||||
import auth.data_impl_fs;
|
import auth.data_impl_fs;
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, "/login")
|
@PathMapping(HttpMethod.POST, "/api/login")
|
||||||
void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
struct LoginData {
|
struct LoginData {
|
||||||
string username;
|
string username;
|
||||||
|
|
@ -26,7 +26,7 @@ struct UsernameAvailabilityResponse {
|
||||||
const bool available;
|
const bool available;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.GET, "/register/username-availability")
|
@PathMapping(HttpMethod.GET, "/api/register/username-availability")
|
||||||
void getUsernameAvailability(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void getUsernameAvailability(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
string username = null;
|
string username = null;
|
||||||
foreach (param; request.queryParams) {
|
foreach (param; request.queryParams) {
|
||||||
|
|
@ -50,7 +50,7 @@ struct RegistrationData {
|
||||||
string password;
|
string password;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PathMapping(HttpMethod.POST, "/register")
|
@PathMapping(HttpMethod.POST, "/api/register")
|
||||||
void postRegister(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void postRegister(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
RegistrationData registrationData = readJsonBodyAs!RegistrationData(request);
|
RegistrationData registrationData = readJsonBodyAs!RegistrationData(request);
|
||||||
if (!validateUsername(registrationData.username)) {
|
if (!validateUsername(registrationData.username)) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
module data_api;
|
module data_api;
|
||||||
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
|
import handy_http_handlers.path_handler : PathMapping;
|
||||||
import handy_http_data;
|
import handy_http_data;
|
||||||
import util.money;
|
import util.money;
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, "/api/currencies")
|
||||||
void handleGetCurrencies(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetCurrencies(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
writeJsonBody(response, ALL_CURRENCIES);
|
writeJsonBody(response, ALL_CURRENCIES);
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import std.json;
|
||||||
import asdf;
|
import asdf;
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import handy_http_data.json;
|
import handy_http_data.json;
|
||||||
import handy_http_handlers.path_handler : getPathParamAs;
|
import handy_http_handlers.path_handler : getPathParamAs, PathMapping;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
import profile.model;
|
import profile.model;
|
||||||
|
|
@ -14,10 +14,13 @@ import profile.data_impl_sqlite;
|
||||||
import auth.model;
|
import auth.model;
|
||||||
import auth.service;
|
import auth.service;
|
||||||
|
|
||||||
|
const PROFILE_PATH = "/api/profiles/:profile";
|
||||||
|
|
||||||
struct NewProfilePayload {
|
struct NewProfilePayload {
|
||||||
string name;
|
string name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.POST, "/api/profiles")
|
||||||
void handleCreateNewProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateNewProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto payload = readJsonBodyAs!NewProfilePayload(request);
|
auto payload = readJsonBodyAs!NewProfilePayload(request);
|
||||||
string name = payload.name;
|
string name = payload.name;
|
||||||
|
|
@ -32,6 +35,7 @@ void handleCreateNewProfile(ref ServerHttpRequest request, ref ServerHttpRespons
|
||||||
writeJsonBody(response, p);
|
writeJsonBody(response, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, "/api/profiles")
|
||||||
void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
AuthContext auth = getAuthContext(request);
|
AuthContext auth = getAuthContext(request);
|
||||||
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
||||||
|
|
@ -39,11 +43,13 @@ void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse res
|
||||||
writeJsonBody(response, profiles);
|
writeJsonBody(response, profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH)
|
||||||
void handleGetProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||||
writeJsonBody(response, profileCtx.profile);
|
writeJsonBody(response, profileCtx.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, PROFILE_PATH)
|
||||||
void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
string name = request.getPathParamAs!string("profile");
|
string name = request.getPathParamAs!string("profile");
|
||||||
if (!validateProfileName(name)) {
|
if (!validateProfileName(name)) {
|
||||||
|
|
@ -56,6 +62,7 @@ void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
profileRepo.deleteByName(name);
|
profileRepo.deleteByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/properties")
|
||||||
void handleGetProperties(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetProperties(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||||
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
||||||
|
|
@ -65,6 +72,7 @@ void handleGetProperties(ref ServerHttpRequest request, ref ServerHttpResponse r
|
||||||
writeJsonBody(response, props);
|
writeJsonBody(response, props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/download")
|
||||||
void handleDownloadProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDownloadProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||||
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import transaction.service;
|
||||||
import transaction.dto;
|
import transaction.dto;
|
||||||
import profile.data;
|
import profile.data;
|
||||||
import profile.service;
|
import profile.service;
|
||||||
|
import profile.api : PROFILE_PATH;
|
||||||
import account.api;
|
import account.api;
|
||||||
import util.money;
|
import util.money;
|
||||||
import util.pagination;
|
import util.pagination;
|
||||||
|
|
@ -21,6 +22,7 @@ import util.data;
|
||||||
|
|
||||||
immutable DEFAULT_TRANSACTION_PAGE = PageRequest(1, 10, [Sort("txn.timestamp", SortDir.DESC)]);
|
immutable DEFAULT_TRANSACTION_PAGE = PageRequest(1, 10, [Sort("txn.timestamp", SortDir.DESC)]);
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions")
|
||||||
void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||||
|
|
@ -28,6 +30,7 @@ void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, responsePage);
|
writeJsonBody(response, responsePage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/search")
|
||||||
void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||||
|
|
@ -35,6 +38,7 @@ void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpRespo
|
||||||
writeJsonBody(response, page);
|
writeJsonBody(response, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request));
|
TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request));
|
||||||
|
|
@ -43,6 +47,7 @@ void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/transactions")
|
||||||
void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import asdf : serializeToJson;
|
import asdf : serializeToJson;
|
||||||
auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request);
|
auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request);
|
||||||
|
|
@ -52,6 +57,7 @@ void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
import asdf : serializeToJson;
|
import asdf : serializeToJson;
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -62,12 +68,14 @@ void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpRespon
|
||||||
response.writeBodyString(jsonStr, "application/json");
|
response.writeBodyString(jsonStr, "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||||
void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
ulong txnId = getTransactionIdOrThrow(request);
|
ulong txnId = getTransactionIdOrThrow(request);
|
||||||
deleteTransaction(ds, txnId);
|
deleteTransaction(ds, txnId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags")
|
||||||
void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
string[] tags = ds.getTransactionTagRepository().findAll();
|
string[] tags = ds.getTransactionTagRepository().findAll();
|
||||||
|
|
@ -80,12 +88,14 @@ private ulong getTransactionIdOrThrow(in ServerHttpRequest request) {
|
||||||
|
|
||||||
// Vendors API
|
// Vendors API
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors")
|
||||||
void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionVendor[] vendors = getAllVendors(ds);
|
TransactionVendor[] vendors = getAllVendors(ds);
|
||||||
writeJsonBody(response, vendors);
|
writeJsonBody(response, vendors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
TransactionVendor vendor = getVendor(ds, getVendorId(request));
|
TransactionVendor vendor = getVendor(ds, getVendorId(request));
|
||||||
|
|
@ -97,6 +107,7 @@ struct VendorPayload {
|
||||||
string description;
|
string description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/vendors")
|
||||||
void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -104,6 +115,7 @@ void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
||||||
writeJsonBody(response, vendor);
|
writeJsonBody(response, vendor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -111,6 +123,7 @@ void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
||||||
writeJsonBody(response, updated);
|
writeJsonBody(response, updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||||
void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
deleteVendor(ds, getVendorId(request));
|
deleteVendor(ds, getVendorId(request));
|
||||||
|
|
@ -122,16 +135,19 @@ private ulong getVendorId(in ServerHttpRequest request) {
|
||||||
|
|
||||||
// Categories API
|
// Categories API
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories")
|
||||||
void handleGetCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
TransactionCategoryTree[] categories = getCategories(getProfileDataSource(request));
|
TransactionCategoryTree[] categories = getCategories(getProfileDataSource(request));
|
||||||
writeJsonBody(response, categories);
|
writeJsonBody(response, categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||||
void handleGetCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto category = getCategory(getProfileDataSource(request), getCategoryId(request));
|
auto category = getCategory(getProfileDataSource(request), getCategoryId(request));
|
||||||
writeJsonBody(response, category);
|
writeJsonBody(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong/children")
|
||||||
void handleGetChildCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetChildCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
auto children = getChildCategories(getProfileDataSource(request), getCategoryId(request));
|
auto children = getChildCategories(getProfileDataSource(request), getCategoryId(request));
|
||||||
writeJsonBody(response, children);
|
writeJsonBody(response, children);
|
||||||
|
|
@ -144,6 +160,7 @@ struct CategoryPayload {
|
||||||
Nullable!ulong parentId;
|
Nullable!ulong parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/categories")
|
||||||
void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -151,6 +168,7 @@ void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, category);
|
writeJsonBody(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||||
void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
|
|
@ -159,6 +177,7 @@ void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
||||||
writeJsonBody(response, category);
|
writeJsonBody(response, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||||
void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
ulong categoryId = getCategoryId(request);
|
ulong categoryId = getCategoryId(request);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue