Compare commits
No commits in common. "409602c825318e2f7c9ed2be4dff03728c21f8fe" and "4130d36472f6bfcb0f9224a09c466be066536f5a" have entirely different histories.
409602c825
...
4130d36472
40
.gitlab-ci.yml
Normal file
40
.gitlab-ci.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
image: rust:1.59-alpine3.15
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- apk --no-cache add musl-dev
|
||||||
|
script:
|
||||||
|
- cargo test
|
||||||
|
|
||||||
|
audit:
|
||||||
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- apk --no-cache add musl-dev cargo-audit
|
||||||
|
script:
|
||||||
|
- cargo audit
|
||||||
|
|
||||||
|
build-statically:
|
||||||
|
stage: build
|
||||||
|
before_script:
|
||||||
|
- apk --no-cache add musl-dev make rustup cargo
|
||||||
|
- rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl x86_64-pc-windows-gnu wasm32-unknown-unknown
|
||||||
|
- cargo install trunk
|
||||||
|
- wget https://musl.cc/aarch64-linux-musl-cross.tgz
|
||||||
|
- echo "c909817856d6ceda86aa510894fa3527eac7989f0ef6e87b5721c58737a06c38 aarch64-linux-musl-cross.tgz" | sha256sum -c - || exit 1
|
||||||
|
- tar -zxvf aarch64-linux-musl-cross.tgz -C / --exclude='aarch64-linux-musl-cross/usr' --strip 1
|
||||||
|
- wget https://musl.cc/x86_64-w64-mingw32-cross.tgz
|
||||||
|
- echo "3a5c90309209a8b2e7ea1715a34b1029692e34189c5e7ecd77e1f102f82f6a02 x86_64-w64-mingw32-cross.tgz" | sha256sum -c - || exit 1
|
||||||
|
- tar -zxvf x86_64-w64-mingw32-cross.tgz -C / --exclude='./x86_64-w64-mingw32-cross/usr' --strip 2
|
||||||
|
- wget https://musl.cc/x86_64-linux-musl-cross.tgz
|
||||||
|
- echo "c5d410d9f82a4f24c549fe5d24f988f85b2679b452413a9f7e5f7b956f2fe7ea x86_64-linux-musl-cross.tgz" | sha256sum -c - || exit 1
|
||||||
|
- tar -zxvf x86_64-linux-musl-cross.tgz -C / --exclude='x86_64-linux-musl-cross/usr' --strip 1
|
||||||
|
script:
|
||||||
|
- make all
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- release/*
|
@ -1,27 +0,0 @@
|
|||||||
pipeline:
|
|
||||||
test:
|
|
||||||
image: docker.io/rust:1.59-alpine3.15
|
|
||||||
commands:
|
|
||||||
- apk --no-cache add musl-dev
|
|
||||||
- cargo test
|
|
||||||
audit:
|
|
||||||
image: docker.io/rust:1.59-alpine3.15
|
|
||||||
commands:
|
|
||||||
- apk --no-cache add musl-dev cargo-audit
|
|
||||||
- cargo audit
|
|
||||||
build-statically:
|
|
||||||
image: docker.io/rust:1.59-alpine3.15
|
|
||||||
commands:
|
|
||||||
- apk --no-cache add musl-dev make rustup cargo
|
|
||||||
- rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl x86_64-pc-windows-gnu wasm32-unknown-unknown
|
|
||||||
- cargo install trunk
|
|
||||||
- wget https://musl.cc/aarch64-linux-musl-cross.tgz
|
|
||||||
- echo "c909817856d6ceda86aa510894fa3527eac7989f0ef6e87b5721c58737a06c38 aarch64-linux-musl-cross.tgz" | sha256sum -c - || exit 1
|
|
||||||
- tar -zxvf aarch64-linux-musl-cross.tgz -C / --exclude='aarch64-linux-musl-cross/usr' --strip 1
|
|
||||||
- wget https://musl.cc/x86_64-w64-mingw32-cross.tgz
|
|
||||||
- echo "3a5c90309209a8b2e7ea1715a34b1029692e34189c5e7ecd77e1f102f82f6a02 x86_64-w64-mingw32-cross.tgz" | sha256sum -c - || exit 1
|
|
||||||
- tar -zxvf x86_64-w64-mingw32-cross.tgz -C / --exclude='./x86_64-w64-mingw32-cross/usr' --strip 2
|
|
||||||
- wget https://musl.cc/x86_64-linux-musl-cross.tgz
|
|
||||||
- echo "c5d410d9f82a4f24c549fe5d24f988f85b2679b452413a9f7e5f7b956f2fe7ea x86_64-linux-musl-cross.tgz" | sha256sum -c - || exit 1
|
|
||||||
- tar -zxvf x86_64-linux-musl-cross.tgz -C / --exclude='x86_64-linux-musl-cross/usr' --strip 1
|
|
||||||
- make all
|
|
@ -1,4 +1,3 @@
|
|||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -26,23 +25,6 @@ pub struct Account {
|
|||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
|
||||||
pub fn hash_string(&self) -> String {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
self.hash(&mut hasher);
|
|
||||||
format!("{:x}", hasher.finish())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for Account {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.number.hash(state);
|
|
||||||
self.owner.hash(state);
|
|
||||||
self.balance.to_string().hash(state);
|
|
||||||
self.is_active.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Account {
|
impl Default for Account {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Account {
|
Account {
|
||||||
@ -60,6 +42,12 @@ impl PartialEq for Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for Account {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.number.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -13,11 +13,11 @@ impl Bank {
|
|||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn account_action<F: Fn(&mut Account) -> Result<R, AccountError>, R>(
|
pub fn account_action<F: Fn(&mut Account) -> Result<f64, AccountError>>(
|
||||||
bank: RwLockReadGuard<'_, Bank>,
|
bank: RwLockReadGuard<'_, Bank>,
|
||||||
nr: &str,
|
nr: &str,
|
||||||
action: F,
|
action: F,
|
||||||
) -> Result<R, AccountError> {
|
) -> Result<f64, AccountError> {
|
||||||
match bank.accounts.get(nr) {
|
match bank.accounts.get(nr) {
|
||||||
None => Err(AccountError::NotFound),
|
None => Err(AccountError::NotFound),
|
||||||
Some(account) => {
|
Some(account) => {
|
||||||
|
@ -4,6 +4,5 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Vesys Bank</title>
|
<title>Vesys Bank</title>
|
||||||
<link data-trunk rel="css" type="text/css" href="styles.css">
|
<link data-trunk rel="css" type="text/css" href="styles.css">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
@ -27,14 +27,8 @@ pub async fn fetch_account(host: &str, nr: &str) -> Result<JsonAccount, FetchErr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_balance(
|
pub async fn set_balance(host: &str, nr: &str, balance: f64) -> Result<(), FetchError> {
|
||||||
host: &str,
|
|
||||||
nr: &str,
|
|
||||||
balance: f64,
|
|
||||||
etag: String,
|
|
||||||
) -> Result<(), FetchError> {
|
|
||||||
let data = JsValue::from_str(&format!("amount={}", balance));
|
let data = JsValue::from_str(&format!("amount={}", balance));
|
||||||
client::put(&format! {"{}/accounts/{}", host, nr}, data, Some(etag)).await?;
|
client::put(&format! {"{}/accounts/{}", host, nr}, data).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ pub async fn get_json(url: &str) -> Result<Option<String>, FetchError> {
|
|||||||
Ok(text.as_string())
|
Ok(text.as_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn put(url: &str, data: JsValue, if_match: Option<String>) -> Result<(), FetchError> {
|
pub async fn put(url: &str, data: JsValue) -> Result<(), FetchError> {
|
||||||
let mut opts = RequestInit::new();
|
let mut opts = RequestInit::new();
|
||||||
opts.method("PUT");
|
opts.method("PUT");
|
||||||
opts.mode(RequestMode::Cors);
|
opts.mode(RequestMode::Cors);
|
||||||
@ -63,11 +63,6 @@ pub async fn put(url: &str, data: JsValue, if_match: Option<String>) -> Result<(
|
|||||||
request
|
request
|
||||||
.headers()
|
.headers()
|
||||||
.set("Content-Type", "application/x-www-form-urlencoded")?;
|
.set("Content-Type", "application/x-www-form-urlencoded")?;
|
||||||
|
|
||||||
if let Some(etag) = if_match {
|
|
||||||
request.headers().set("If-match", &etag)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self::send_request(request).await?;
|
self::send_request(request).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -159,9 +159,9 @@ impl Component for Account {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick={on_deposit} class={classes!("account__button", "account__deposit")} disabled={!self.amount_valid}>{"deposit"}</button>
|
<button onclick={on_deposit} class={classes!("account__button")} disabled={!self.amount_valid}>{"deposit"}</button>
|
||||||
<button onclick={on_withdraw} class={classes!("account__button", "account__withdraw")} disabled={!self.amount_valid}>{"withdraw"}</button>
|
<button onclick={on_withdraw} class={classes!("account__button")} disabled={!self.amount_valid}>{"withdraw"}</button>
|
||||||
<button onclick={on_transfer} class={classes!("account__button", "account__transfer")} disabled={!self.amount_valid || !self.is_transfer_account_valid()}>{"transfer"}</button>
|
<button onclick={on_transfer} class={classes!("account__button")} disabled={!self.amount_valid || !self.is_transfer_account_valid()}>{"transfer"}</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
@ -40,13 +40,9 @@ impl Component for Accounts {
|
|||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
if ctx.props().account_nrs.is_empty() {
|
<ul class={classes!("accounts")}>
|
||||||
<h2>{"No accounts"}</h2>
|
{ for ctx.props().account_nrs.iter().map(|e| self.account_entry(e, &ctx.props().selected_nr, ctx)) }
|
||||||
} else {
|
</ul>
|
||||||
<ul class={classes!("accounts")}>
|
|
||||||
{ for ctx.props().account_nrs.iter().map(|e| self.account_entry(e, &ctx.props().selected_nr, ctx)) }
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,17 +77,8 @@ impl Component for Main {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
Event::SetBalance(balance, nr) => {
|
Event::SetBalance(balance, nr) => {
|
||||||
let acc = bank::account::Account {
|
|
||||||
number: nr.clone(),
|
|
||||||
owner: self.selected_owner.clone(),
|
|
||||||
balance: self.selected_balance,
|
|
||||||
is_active: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let hash = acc.hash_string();
|
|
||||||
|
|
||||||
ctx.link().send_future(async move {
|
ctx.link().send_future(async move {
|
||||||
match api::set_balance("http://localhost:8000", &nr, balance, hash).await {
|
match api::set_balance("http://localhost:8000", &nr, balance).await {
|
||||||
Err(e) => Event::ShowError(e.to_string()),
|
Err(e) => Event::ShowError(e.to_string()),
|
||||||
Ok(_) => Event::SelectAccountNr(nr),
|
Ok(_) => Event::SelectAccountNr(nr),
|
||||||
}
|
}
|
||||||
@ -108,12 +99,8 @@ impl Component for Main {
|
|||||||
<h1 class={classes!("title")}>
|
<h1 class={classes!("title")}>
|
||||||
{"welcome to your vaults"}
|
{"welcome to your vaults"}
|
||||||
</h1>
|
</h1>
|
||||||
<div class={classes!("inner-content")}>
|
<Accounts account_nrs={self.account_nrs.clone()} selected_nr={self.selected_nr.clone()} />
|
||||||
<Accounts account_nrs={self.account_nrs.clone()} selected_nr={self.selected_nr.clone()} />
|
<Account balance={self.selected_balance} owner={self.selected_owner.clone()} nr={self.selected_nr.clone()} account_nrs={self.account_nrs.clone()} />
|
||||||
if !self.account_nrs.is_empty() {
|
|
||||||
<Account balance={self.selected_balance} owner={self.selected_owner.clone()} nr={self.selected_nr.clone()} account_nrs={self.account_nrs.clone()} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -7,19 +7,12 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.inner-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: black;
|
border-color: black;
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__grid {
|
.account__grid {
|
||||||
@ -32,7 +25,6 @@ html, body {
|
|||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
margin-left: 7px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__label {
|
.account__label {
|
||||||
@ -55,21 +47,6 @@ html, body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__deposit {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__withdraw {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__transfer {
|
|
||||||
grid-column: 3;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__button {
|
.account__button {
|
||||||
color: black;
|
color: black;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -109,9 +86,6 @@ html, body {
|
|||||||
|
|
||||||
.accounts {
|
.accounts {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
display: inline-block;
|
|
||||||
min-width: min-content;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: black;
|
border-color: black;
|
||||||
@ -140,19 +114,14 @@ html, body {
|
|||||||
min-width: 370px;
|
min-width: 370px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1005px) {
|
@media (max-width: 605px) {
|
||||||
.inner-content {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 615px) {
|
|
||||||
.accounts {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__grid {
|
.account__grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account__accounts {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use actix_web::{put, web, HttpRequest, HttpResponse, Responder, Result};
|
use actix_web::{put, web, HttpResponse, Responder, Result};
|
||||||
use bank::account::AccountError;
|
use bank::account::AccountError;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use bank::bank::Bank;
|
||||||
|
|
||||||
use crate::handlers::error::HttpAccountError;
|
use crate::handlers::error::HttpAccountError;
|
||||||
use crate::handlers::AmountData;
|
use crate::handlers::AmountData;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -16,7 +18,6 @@ pub async fn route(
|
|||||||
info: web::Path<String>,
|
info: web::Path<String>,
|
||||||
form: web::Form<AmountData>,
|
form: web::Form<AmountData>,
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
req: HttpRequest,
|
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
let nr = info.into_inner();
|
let nr = info.into_inner();
|
||||||
let amount = form.amount;
|
let amount = form.amount;
|
||||||
@ -28,22 +29,11 @@ pub async fn route(
|
|||||||
info!("updating account {}...", nr);
|
info!("updating account {}...", nr);
|
||||||
|
|
||||||
let bank = data.bank.read().unwrap();
|
let bank = data.bank.read().unwrap();
|
||||||
match bank.accounts.get(&nr) {
|
match Bank::account_action(bank, &nr, |account| {
|
||||||
None => Err(HttpAccountError(AccountError::NotFound).into()),
|
account.balance = amount;
|
||||||
Some(account) => {
|
Ok(amount)
|
||||||
let mut account = account.write().unwrap();
|
}) {
|
||||||
let hash = account.hash_string();
|
Err(e) => Err(HttpAccountError(e).into()),
|
||||||
if let Some(etag) = req.headers().get("If-Match") {
|
Ok(_) => Ok(HttpResponse::Ok().finish()),
|
||||||
let etag = etag.to_str().unwrap_or("");
|
|
||||||
if etag != hash {
|
|
||||||
Ok(HttpResponse::PreconditionFailed().finish())
|
|
||||||
} else {
|
|
||||||
account.balance = amount;
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(HttpResponse::PreconditionRequired().finish())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,30 +155,6 @@ mod tests {
|
|||||||
.set_form(&payload)
|
.set_form(&payload)
|
||||||
.to_request();
|
.to_request();
|
||||||
let resp = test::call_service(&app, req).await;
|
let resp = test::call_service(&app, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_REQUIRED);
|
|
||||||
|
|
||||||
let payload = AmountData { amount: 10_f64 };
|
|
||||||
let req = test::TestRequest::put()
|
|
||||||
.uri(&format!("/accounts/{}", nr))
|
|
||||||
.append_header((actix_web::http::header::IF_MATCH, "I aspire to be an etag"))
|
|
||||||
.set_form(&payload)
|
|
||||||
.to_request();
|
|
||||||
let resp = test::call_service(&app, req).await;
|
|
||||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
|
||||||
|
|
||||||
let acc = bank::account::Account {
|
|
||||||
number: nr.to_string(),
|
|
||||||
owner: "aaa".to_string(),
|
|
||||||
balance: 5_f64,
|
|
||||||
is_active: true,
|
|
||||||
};
|
|
||||||
let payload = AmountData { amount: 10_f64 };
|
|
||||||
let req = test::TestRequest::put()
|
|
||||||
.uri(&format!("/accounts/{}", nr))
|
|
||||||
.append_header((actix_web::http::header::IF_MATCH, acc.hash_string()))
|
|
||||||
.set_form(&payload)
|
|
||||||
.to_request();
|
|
||||||
let resp = test::call_service(&app, req).await;
|
|
||||||
assert!(resp.status().is_success());
|
assert!(resp.status().is_success());
|
||||||
|
|
||||||
let req = test::TestRequest::put()
|
let req = test::TestRequest::put()
|
||||||
|
Loading…
Reference in New Issue
Block a user