Add internal transfer label, convert to filters for request handling.
This commit is contained in:
parent
cb690702cc
commit
ad92f6f9dd
|
|
@ -3,6 +3,7 @@ module api_mapping;
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import handy_http_handlers.path_handler;
|
import handy_http_handlers.path_handler;
|
||||||
import handy_http_handlers.filtered_handler;
|
import handy_http_handlers.filtered_handler;
|
||||||
|
import slf4d;
|
||||||
|
|
||||||
/// The base path to all API endpoints.
|
/// The base path to all API endpoints.
|
||||||
private const API_PATH = "/api";
|
private const API_PATH = "/api";
|
||||||
|
|
@ -101,7 +102,15 @@ HttpRequestHandler mapApiHandlers(string webOrigin) {
|
||||||
a
|
a
|
||||||
));
|
));
|
||||||
|
|
||||||
return new CorsHandler(h, webOrigin);
|
// Build the main handler into a filter chain:
|
||||||
|
return new FilteredHandler(
|
||||||
|
[
|
||||||
|
cast(HttpRequestFilter) new CorsFilter(webOrigin),
|
||||||
|
cast(HttpRequestFilter) new ContentLengthFilter(),
|
||||||
|
cast(HttpRequestFilter) new ExceptionHandlingFilter()
|
||||||
|
],
|
||||||
|
h
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getStatus(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
private void getStatus(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
|
@ -136,26 +145,53 @@ private void map(
|
||||||
handler.addMapping(method, API_PATH ~ subPath, HttpRequestHandler.of(fn));
|
handler.addMapping(method, API_PATH ~ subPath, HttpRequestHandler.of(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CorsHandler : HttpRequestHandler {
|
private class CorsFilter : HttpRequestFilter {
|
||||||
private HttpRequestHandler handler;
|
|
||||||
private string webOrigin;
|
private string webOrigin;
|
||||||
|
|
||||||
this(HttpRequestHandler handler, string webOrigin) {
|
this(string webOrigin) {
|
||||||
this.handler = handler;
|
|
||||||
this.webOrigin = webOrigin;
|
this.webOrigin = webOrigin;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
|
||||||
response.headers.add("Access-Control-Allow-Origin", webOrigin);
|
response.headers.add("Access-Control-Allow-Origin", webOrigin);
|
||||||
response.headers.add("Access-Control-Allow-Methods", "*");
|
response.headers.add("Access-Control-Allow-Methods", "*");
|
||||||
response.headers.add("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
response.headers.add("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContentLengthFilter : HttpRequestFilter {
|
||||||
|
const MAX_LENGTH = 1024 * 1024 * 20; // 2MB limit
|
||||||
|
|
||||||
|
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
|
||||||
|
if ("Content-Length" in request.headers) {
|
||||||
|
ulong contentLength = request.getHeaderAs!ulong("Content-Length");
|
||||||
|
if (contentLength > MAX_LENGTH) {
|
||||||
|
warnF!"Received request with content length of %d, larger than max allowed %d bytes."(
|
||||||
|
contentLength,
|
||||||
|
MAX_LENGTH
|
||||||
|
);
|
||||||
|
import std.conv;
|
||||||
|
response.status = HttpStatus.PAYLOAD_TOO_LARGE;
|
||||||
|
response.writeBodyString(
|
||||||
|
"Request body is too large. Must be at most " ~ MAX_LENGTH.to!string ~ " bytes."
|
||||||
|
);
|
||||||
|
return; // Don't propagate the filter.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExceptionHandlingFilter : HttpRequestFilter {
|
||||||
|
void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
|
||||||
try {
|
try {
|
||||||
this.handler.handle(request, response);
|
filterChain.doFilter(request, response);
|
||||||
} catch (HttpStatusException e) {
|
} catch (HttpStatusException e) {
|
||||||
response.status = e.status;
|
response.status = e.status;
|
||||||
response.writeBodyString(e.message.idup);
|
response.writeBodyString(e.message.idup);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
import slf4d;
|
|
||||||
error(e);
|
error(e);
|
||||||
response.status = HttpStatus.INTERNAL_SERVER_ERROR;
|
response.status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
response.writeBodyString("An error occurred: " ~ e.msg);
|
response.writeBodyString("An error occurred: " ~ e.msg);
|
||||||
|
|
|
||||||
|
|
@ -87,58 +87,34 @@ async function deleteTransaction() {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<AppPage
|
<AppPage :title="'Transaction ' + transaction.id" v-if="transaction">
|
||||||
:title="'Transaction ' + transaction.id"
|
|
||||||
v-if="transaction"
|
|
||||||
>
|
|
||||||
<!-- Top-row with some badges for amount, vendor, and category. -->
|
<!-- Top-row with some badges for amount, vendor, and category. -->
|
||||||
<div>
|
<div>
|
||||||
<AppBadge
|
<AppBadge size="lg" class="font-mono">
|
||||||
size="lg"
|
|
||||||
class="font-mono"
|
|
||||||
>
|
|
||||||
{{ transaction.currency.code }} {{ formatMoney(transaction.amount, transaction.currency) }}
|
{{ transaction.currency.code }} {{ formatMoney(transaction.amount, transaction.currency) }}
|
||||||
</AppBadge>
|
</AppBadge>
|
||||||
<AppBadge
|
<AppBadge size="md" v-if="transaction.vendor">
|
||||||
size="md"
|
|
||||||
v-if="transaction.vendor"
|
|
||||||
>
|
|
||||||
{{ transaction.vendor.name }}
|
{{ transaction.vendor.name }}
|
||||||
</AppBadge>
|
</AppBadge>
|
||||||
<CategoryLabel
|
<CategoryLabel v-if="transaction.category" :category="transaction.category" :clickable="true" />
|
||||||
v-if="transaction.category"
|
<AppBadge size="sm" v-if="transaction.internalTransfer">
|
||||||
:category="transaction.category"
|
<font-awesome-icon icon="fa-rotate"></font-awesome-icon>
|
||||||
:clickable="true"
|
Internal Transfer
|
||||||
/>
|
</AppBadge>
|
||||||
</div>
|
</div>
|
||||||
<!-- Second row that lists all tags. -->
|
<!-- Second row that lists all tags. -->
|
||||||
<div
|
<div v-if="transaction.tags.length > 0" class="mt-1">
|
||||||
v-if="transaction.tags.length > 0"
|
<TagLabel v-for="t in transaction.tags" :key="t" :tag="t" />
|
||||||
class="mt-1"
|
|
||||||
>
|
|
||||||
<TagLabel
|
|
||||||
v-for="t in transaction.tags"
|
|
||||||
:key="t"
|
|
||||||
:tag="t"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>{{ transaction.description }}</p>
|
<p>{{ transaction.description }}</p>
|
||||||
|
|
||||||
<div
|
<div v-if="transaction.creditedAccount" class="my-1">
|
||||||
v-if="transaction.creditedAccount"
|
|
||||||
class="my-1"
|
|
||||||
>
|
|
||||||
<strong class="text-negative">Credited</strong> from
|
<strong class="text-negative">Credited</strong> from
|
||||||
<RouterLink
|
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`">
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`"
|
|
||||||
>
|
|
||||||
{{ transaction.creditedAccount.name }} (#{{ transaction.creditedAccount.numberSuffix }})
|
{{ transaction.creditedAccount.name }} (#{{ transaction.creditedAccount.numberSuffix }})
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div
|
<div v-if="creditedAccountBalanceDiff" class="font-size-xsmall">
|
||||||
v-if="creditedAccountBalanceDiff"
|
|
||||||
class="font-size-xsmall"
|
|
||||||
>
|
|
||||||
Balance Before:
|
Balance Before:
|
||||||
<span class="font-mono">
|
<span class="font-mono">
|
||||||
{{ formatMoney(creditedAccountBalanceDiff.before, transaction.currency) }}
|
{{ formatMoney(creditedAccountBalanceDiff.before, transaction.currency) }}
|
||||||
|
|
@ -150,20 +126,12 @@ async function deleteTransaction() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="transaction.debitedAccount" class="my-1">
|
||||||
v-if="transaction.debitedAccount"
|
|
||||||
class="my-1"
|
|
||||||
>
|
|
||||||
<strong class="text-positive">Debited</strong> to
|
<strong class="text-positive">Debited</strong> to
|
||||||
<RouterLink
|
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`">
|
||||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`"
|
|
||||||
>
|
|
||||||
{{ transaction.debitedAccount.name }} (#{{ transaction.debitedAccount.numberSuffix }})
|
{{ transaction.debitedAccount.name }} (#{{ transaction.debitedAccount.numberSuffix }})
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div
|
<div v-if="debitedAccountBalanceDiff" class="font-size-xsmall">
|
||||||
v-if="debitedAccountBalanceDiff"
|
|
||||||
class="font-size-xsmall"
|
|
||||||
>
|
|
||||||
Balance Before:
|
Balance Before:
|
||||||
<span class="font-mono">
|
<span class="font-mono">
|
||||||
{{ formatMoney(debitedAccountBalanceDiff.before, transaction.currency) }}
|
{{ formatMoney(debitedAccountBalanceDiff.before, transaction.currency) }}
|
||||||
|
|
@ -189,39 +157,21 @@ async function deleteTransaction() {
|
||||||
|
|
||||||
<div v-if="transaction.lineItems.length > 0">
|
<div v-if="transaction.lineItems.length > 0">
|
||||||
<h3>Line Items</h3>
|
<h3>Line Items</h3>
|
||||||
<LineItemCard
|
<LineItemCard v-for="item of transaction.lineItems" :key="item.idx" :line-item="item"
|
||||||
v-for="item of transaction.lineItems"
|
:currency="transaction.currency" :total-count="transaction.lineItems.length" :editable="false" />
|
||||||
:key="item.idx"
|
|
||||||
:line-item="item"
|
|
||||||
:currency="transaction.currency"
|
|
||||||
:total-count="transaction.lineItems.length"
|
|
||||||
:editable="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="transaction.attachments.length > 0">
|
<div v-if="transaction.attachments.length > 0">
|
||||||
<h3>Attachments</h3>
|
<h3>Attachments</h3>
|
||||||
<AttachmentRow
|
<AttachmentRow v-for="a in transaction.attachments" :attachment="a" :key="a.id" disabled />
|
||||||
v-for="a in transaction.attachments"
|
|
||||||
:attachment="a"
|
|
||||||
:key="a.id"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ButtonBar>
|
<ButtonBar>
|
||||||
<AppButton
|
<AppButton icon="wrench" @click="
|
||||||
icon="wrench"
|
router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)
|
||||||
@click="
|
">
|
||||||
router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Edit
|
Edit
|
||||||
</AppButton>
|
</AppButton>
|
||||||
<AppButton
|
<AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton>
|
||||||
icon="trash"
|
|
||||||
@click="deleteTransaction()"
|
|
||||||
>Delete</AppButton
|
|
||||||
>
|
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
</AppPage>
|
</AppPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue