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.data;
|
||||
import profile.api : PROFILE_PATH;
|
||||
import account.model;
|
||||
import account.service;
|
||||
import account.dto;
|
||||
|
|
@ -21,6 +22,9 @@ import account.data;
|
|||
import attachment.data;
|
||||
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) {
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
|
|
@ -30,6 +34,7 @@ void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse res
|
|||
writeJsonBody(response, accounts);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH)
|
||||
void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/accounts")
|
||||
void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto ds = getProfileDataSource(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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.PUT, ACCOUNT_PATH)
|
||||
void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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)));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, ACCOUNT_PATH)
|
||||
void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
auto ds = getProfileDataSource(request);
|
||||
ds.getAccountRepository().deleteById(accountId);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/balance")
|
||||
void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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) {
|
||||
ulong accountId = request.getPathParamOrThrow!ulong("accountId");
|
||||
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);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/account-balances")
|
||||
void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto ds = getProfileDataSource(request);
|
||||
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)]);
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records")
|
||||
void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
auto ds = getProfileDataSource(request);
|
||||
|
|
@ -152,6 +164,7 @@ void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
writeJsonBody(response, page);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong")
|
||||
void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
||||
|
|
@ -162,6 +175,7 @@ void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
writeJsonBody(response, AccountValueRecordResponse.of(record, attachmentRepo));
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records")
|
||||
void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
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) {
|
||||
ulong accountId = request.getPathParamAs!ulong("accountId");
|
||||
ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId");
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
module analytics.api;
|
||||
|
||||
import handy_http_primitives;
|
||||
import handy_http_handlers.path_handler : PathMapping;
|
||||
|
||||
import profile.data;
|
||||
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) {
|
||||
auto ds = getProfileDataSource(request);
|
||||
serveJsonFromProperty(response, ds, "analytics.balanceTimeSeries");
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/analytics/category-spend-time-series")
|
||||
void handleGetCategorySpendTimeSeries(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto ds = getProfileDataSource(request);
|
||||
serveJsonFromProperty(response, ds, "analytics.categorySpendTimeSeries");
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import handy_http_handlers.path_handler;
|
|||
import handy_http_handlers.filtered_handler;
|
||||
import slf4d;
|
||||
|
||||
/// The base path to all API endpoints.
|
||||
private const API_PATH = "/api";
|
||||
|
||||
/**
|
||||
* Defines the Finnow API mapping with a main PathHandler.
|
||||
* Params:
|
||||
|
|
@ -15,92 +12,41 @@ private const API_PATH = "/api";
|
|||
* Returns: The handler to plug into an HttpServer.
|
||||
*/
|
||||
HttpRequestHandler mapApiHandlers(string webOrigin) {
|
||||
PathHandler h = new PathHandler();
|
||||
PathHandler publicHandler = new PathHandler();
|
||||
PathHandler authenticatedHandler = new PathHandler();
|
||||
|
||||
// Generic, public endpoints:
|
||||
h.map(HttpMethod.GET, "/status", &getStatus);
|
||||
h.map(HttpMethod.OPTIONS, "/**", &getOptions);
|
||||
// Public endpoints:
|
||||
publicHandler.addMapping(HttpMethod.GET, "/api/status", HttpRequestHandler.of(&getStatus));
|
||||
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!!!
|
||||
// h.map(HttpMethod.POST, "/sample-data", &sampleDataEndpoint);
|
||||
|
||||
// Auth endpoints:
|
||||
import auth.api;
|
||||
import auth.api_public;
|
||||
h.registerHandlers!(auth.api_public);
|
||||
|
||||
// Authenticated endpoints:
|
||||
PathHandler a = new PathHandler();
|
||||
a.registerHandlers!(auth.api);
|
||||
|
||||
import auth.api;
|
||||
authenticatedHandler.registerHandlers!(auth.api);
|
||||
import profile.api;
|
||||
a.map(HttpMethod.GET, "/profiles", &handleGetProfiles);
|
||||
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:
|
||||
authenticatedHandler.registerHandlers!(profile.api);
|
||||
import account.api;
|
||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
||||
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);
|
||||
|
||||
authenticatedHandler.registerHandlers!(account.api);
|
||||
import transaction.api;
|
||||
// Transaction vendor endpoints:
|
||||
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:
|
||||
authenticatedHandler.registerHandlers!(transaction.api);
|
||||
import analytics.api;
|
||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/analytics/balance-time-series", &handleGetBalanceTimeSeries);
|
||||
a.map(HttpMethod.GET, PROFILE_PATH ~ "/analytics/category-spend-time-series", &handleGetCategorySpendTimeSeries);
|
||||
|
||||
authenticatedHandler.registerHandlers!(analytics.api);
|
||||
import data_api;
|
||||
// Various other data endpoints:
|
||||
a.map(HttpMethod.GET, "/currencies", &handleGetCurrencies);
|
||||
authenticatedHandler.registerHandlers!(data_api);
|
||||
|
||||
// Protect all authenticated paths with a filter.
|
||||
import auth.service : AuthenticationFilter;
|
||||
HttpRequestFilter authenticationFilter = new AuthenticationFilter();
|
||||
h.addMapping(API_PATH ~ "/**", new FilteredHandler(
|
||||
publicHandler.addMapping("/api/**", new FilteredHandler(
|
||||
[authenticationFilter],
|
||||
a
|
||||
authenticatedHandler
|
||||
));
|
||||
|
||||
// Build the main handler into a filter chain:
|
||||
|
|
@ -108,9 +54,10 @@ HttpRequestHandler mapApiHandlers(string webOrigin) {
|
|||
[
|
||||
cast(HttpRequestFilter) new CorsFilter(webOrigin),
|
||||
cast(HttpRequestFilter) new ContentLengthFilter(),
|
||||
cast(HttpRequestFilter) new TokenBucketRateLimitingFilter(10, 50),
|
||||
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.");
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
|
@ -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) {
|
||||
Level logLevel = getConfiguredLoggingLevel(config);
|
||||
logLevel = Levels.DEBUG;
|
||||
// logLevel = Levels.DEBUG;
|
||||
auto provider = new DefaultProvider(logLevel);
|
||||
configureLoggingProvider(provider);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ module attachment.api;
|
|||
|
||||
import handy_http_primitives;
|
||||
import handy_http_data.json;
|
||||
import handy_http_handlers.path_handler : PathMapping;
|
||||
import std.conv;
|
||||
|
||||
import profile.data;
|
||||
|
|
@ -22,6 +23,7 @@ import attachment.model;
|
|||
* request = The HTTP request.
|
||||
* response = The HTTP response.
|
||||
*/
|
||||
@PathMapping(HttpMethod.GET, "/api/profiles/:profile/attachments/:attachmentId/download")
|
||||
void handleDownloadAttachment(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
Optional!AuthContext authCtx = extractAuthContextFromQueryParam(request, response);
|
||||
if (authCtx.isNull) return;
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import auth.data;
|
|||
import auth.service;
|
||||
import auth.data_impl_fs;
|
||||
|
||||
@PathMapping(HttpMethod.GET, "/me")
|
||||
@PathMapping(HttpMethod.GET, "/api/me")
|
||||
void getMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
AuthContext auth = getAuthContext(request);
|
||||
response.writeBodyString(auth.user.username);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, "/me")
|
||||
@PathMapping(HttpMethod.DELETE, "/api/me")
|
||||
void deleteMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
AuthContext auth = getAuthContext(request);
|
||||
UserRepository userRepo = new FileSystemUserRepository();
|
||||
|
|
@ -25,7 +25,7 @@ void deleteMyUser(ref ServerHttpRequest request, ref ServerHttpResponse response
|
|||
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) {
|
||||
AuthContext auth = getAuthContext(request);
|
||||
string token = generateTokenForUser(auth.user);
|
||||
|
|
@ -38,7 +38,7 @@ struct PasswordChangeRequest {
|
|||
string newPassword;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, "/me/password")
|
||||
@PathMapping(HttpMethod.POST, "/api/me/password")
|
||||
void changeMyPassword(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
AuthContext auth = getAuthContext(request);
|
||||
PasswordChangeRequest data = readJsonBodyAs!PasswordChangeRequest(request);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import auth.data;
|
|||
import auth.service;
|
||||
import auth.data_impl_fs;
|
||||
|
||||
@PathMapping(HttpMethod.POST, "/login")
|
||||
@PathMapping(HttpMethod.POST, "/api/login")
|
||||
void postLogin(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
struct LoginData {
|
||||
string username;
|
||||
|
|
@ -26,7 +26,7 @@ struct UsernameAvailabilityResponse {
|
|||
const bool available;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, "/register/username-availability")
|
||||
@PathMapping(HttpMethod.GET, "/api/register/username-availability")
|
||||
void getUsernameAvailability(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
string username = null;
|
||||
foreach (param; request.queryParams) {
|
||||
|
|
@ -50,7 +50,7 @@ struct RegistrationData {
|
|||
string password;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, "/register")
|
||||
@PathMapping(HttpMethod.POST, "/api/register")
|
||||
void postRegister(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
RegistrationData registrationData = readJsonBodyAs!RegistrationData(request);
|
||||
if (!validateUsername(registrationData.username)) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
module data_api;
|
||||
|
||||
import handy_http_primitives;
|
||||
import handy_http_handlers.path_handler : PathMapping;
|
||||
import handy_http_data;
|
||||
import util.money;
|
||||
|
||||
@PathMapping(HttpMethod.GET, "/api/currencies")
|
||||
void handleGetCurrencies(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
writeJsonBody(response, ALL_CURRENCIES);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import std.json;
|
|||
import asdf;
|
||||
import handy_http_primitives;
|
||||
import handy_http_data.json;
|
||||
import handy_http_handlers.path_handler : getPathParamAs;
|
||||
import handy_http_handlers.path_handler : getPathParamAs, PathMapping;
|
||||
import slf4d;
|
||||
|
||||
import profile.model;
|
||||
|
|
@ -14,10 +14,13 @@ import profile.data_impl_sqlite;
|
|||
import auth.model;
|
||||
import auth.service;
|
||||
|
||||
const PROFILE_PATH = "/api/profiles/:profile";
|
||||
|
||||
struct NewProfilePayload {
|
||||
string name;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, "/api/profiles")
|
||||
void handleCreateNewProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto payload = readJsonBodyAs!NewProfilePayload(request);
|
||||
string name = payload.name;
|
||||
|
|
@ -32,6 +35,7 @@ void handleCreateNewProfile(ref ServerHttpRequest request, ref ServerHttpRespons
|
|||
writeJsonBody(response, p);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, "/api/profiles")
|
||||
void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
AuthContext auth = getAuthContext(request);
|
||||
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
||||
|
|
@ -39,11 +43,13 @@ void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse res
|
|||
writeJsonBody(response, profiles);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH)
|
||||
void handleGetProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||
writeJsonBody(response, profileCtx.profile);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH)
|
||||
void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
string name = request.getPathParamAs!string("profile");
|
||||
if (!validateProfileName(name)) {
|
||||
|
|
@ -56,6 +62,7 @@ void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse r
|
|||
profileRepo.deleteByName(name);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/properties")
|
||||
void handleGetProperties(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
||||
|
|
@ -65,6 +72,7 @@ void handleGetProperties(ref ServerHttpRequest request, ref ServerHttpResponse r
|
|||
writeJsonBody(response, props);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/download")
|
||||
void handleDownloadProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileContext profileCtx = getProfileContextOrThrow(request);
|
||||
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import transaction.service;
|
|||
import transaction.dto;
|
||||
import profile.data;
|
||||
import profile.service;
|
||||
import profile.api : PROFILE_PATH;
|
||||
import account.api;
|
||||
import util.money;
|
||||
import util.pagination;
|
||||
|
|
@ -21,6 +22,7 @@ import util.data;
|
|||
|
||||
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) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||
|
|
@ -28,6 +30,7 @@ void handleGetTransactions(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
writeJsonBody(response, responsePage);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/search")
|
||||
void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
PageRequest pr = PageRequest.parse(request, DEFAULT_TRANSACTION_PAGE);
|
||||
|
|
@ -35,6 +38,7 @@ void handleSearchTransactions(ref ServerHttpRequest request, ref ServerHttpRespo
|
|||
writeJsonBody(response, page);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||
void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
TransactionDetail txn = getTransaction(ds, getTransactionIdOrThrow(request));
|
||||
|
|
@ -43,6 +47,7 @@ void handleGetTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
response.writeBodyString(jsonStr, "application/json");
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/transactions")
|
||||
void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
import asdf : serializeToJson;
|
||||
auto fullPayload = parseMultipartFilesAndBody!AddTransactionPayload(request);
|
||||
|
|
@ -52,6 +57,7 @@ void handleAddTransaction(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
response.writeBodyString(jsonStr, "application/json");
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||
void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
import asdf : serializeToJson;
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
|
|
@ -62,12 +68,14 @@ void handleUpdateTransaction(ref ServerHttpRequest request, ref ServerHttpRespon
|
|||
response.writeBodyString(jsonStr, "application/json");
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong")
|
||||
void handleDeleteTransaction(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
ulong txnId = getTransactionIdOrThrow(request);
|
||||
deleteTransaction(ds, txnId);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags")
|
||||
void handleGetAllTags(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
string[] tags = ds.getTransactionTagRepository().findAll();
|
||||
|
|
@ -80,12 +88,14 @@ private ulong getTransactionIdOrThrow(in ServerHttpRequest request) {
|
|||
|
||||
// Vendors API
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors")
|
||||
void handleGetVendors(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
TransactionVendor[] vendors = getAllVendors(ds);
|
||||
writeJsonBody(response, vendors);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||
void handleGetVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
TransactionVendor vendor = getVendor(ds, getVendorId(request));
|
||||
|
|
@ -97,6 +107,7 @@ struct VendorPayload {
|
|||
string description;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/vendors")
|
||||
void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
|
|
@ -104,6 +115,7 @@ void handleCreateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
|||
writeJsonBody(response, vendor);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||
void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
VendorPayload payload = readJsonBodyAs!VendorPayload(request);
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
|
|
@ -111,6 +123,7 @@ void handleUpdateVendor(ref ServerHttpRequest request, ref ServerHttpResponse re
|
|||
writeJsonBody(response, updated);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong")
|
||||
void handleDeleteVendor(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
deleteVendor(ds, getVendorId(request));
|
||||
|
|
@ -122,16 +135,19 @@ private ulong getVendorId(in ServerHttpRequest request) {
|
|||
|
||||
// Categories API
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories")
|
||||
void handleGetCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
TransactionCategoryTree[] categories = getCategories(getProfileDataSource(request));
|
||||
writeJsonBody(response, categories);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||
void handleGetCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto category = getCategory(getProfileDataSource(request), getCategoryId(request));
|
||||
writeJsonBody(response, category);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong/children")
|
||||
void handleGetChildCategories(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
auto children = getChildCategories(getProfileDataSource(request), getCategoryId(request));
|
||||
writeJsonBody(response, children);
|
||||
|
|
@ -144,6 +160,7 @@ struct CategoryPayload {
|
|||
Nullable!ulong parentId;
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/categories")
|
||||
void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
|
|
@ -151,6 +168,7 @@ void handleCreateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
writeJsonBody(response, category);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||
void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
CategoryPayload payload = readJsonBodyAs!CategoryPayload(request);
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
|
|
@ -159,6 +177,7 @@ void handleUpdateCategory(ref ServerHttpRequest request, ref ServerHttpResponse
|
|||
writeJsonBody(response, category);
|
||||
}
|
||||
|
||||
@PathMapping(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong")
|
||||
void handleDeleteCategory(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||
ProfileDataSource ds = getProfileDataSource(request);
|
||||
ulong categoryId = getCategoryId(request);
|
||||
|
|
|
|||
Loading…
Reference in New Issue