From 161af74fd88d96a30616338858c9bb825fc85e79 Mon Sep 17 00:00:00 2001
From: Tobias de Bruijn
Date: Wed, 23 Oct 2024 16:43:10 +0200
Subject: [PATCH] Fix frontend to new routes, add random code to email, add
digidecs uploaded page
---
.github/workflows/release.yml | 83 +++++++++++
.github/workflows/test.yml | 53 +++++++
frontend/index.html | 2 +-
frontend/src/plugins/locales/en.ts | 40 +++++-
frontend/src/plugins/locales/locale.ts | 5 +-
frontend/src/plugins/locales/nl.ts | 7 +-
frontend/src/router/index.ts | 4 +
frontend/src/scripts/digidecs.ts | 76 +++++++---
frontend/src/views/HomeView.vue | 46 +++++--
frontend/src/views/SubmittedView.vue | 31 +++++
server/Cargo.lock | 56 ++++----
server/Cargo.toml | 6 +-
server/src/email/ipv4.rs | 10 +-
server/src/email/mod.rs | 28 ++--
server/src/email/template.rs | 6 +-
server/src/file/config.rs | 6 +-
server/src/server/mod.rs | 32 ++---
server/src/server/routes/digidecs.rs | 108 ---------------
.../src/server/routes/digidecs/attachment.rs | 34 +++++
server/src/server/routes/digidecs/complete.rs | 83 +++++++++++
server/src/server/routes/digidecs/mod.rs | 20 +++
server/src/server/routes/digidecs/start.rs | 130 ++++++++++++++++++
server/src/server/routes/mod.rs | 6 +-
server/src/server/types/data.rs | 31 +++++
server/src/server/types/error.rs | 11 +-
server/src/server/types/mod.rs | 2 +-
26 files changed, 697 insertions(+), 219 deletions(-)
create mode 100644 .github/workflows/release.yml
create mode 100644 .github/workflows/test.yml
create mode 100644 frontend/src/views/SubmittedView.vue
delete mode 100644 server/src/server/routes/digidecs.rs
create mode 100644 server/src/server/routes/digidecs/attachment.rs
create mode 100644 server/src/server/routes/digidecs/complete.rs
create mode 100644 server/src/server/routes/digidecs/mod.rs
create mode 100644 server/src/server/routes/digidecs/start.rs
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..1d653bd
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,83 @@
+name: release
+on:
+ push:
+ tags:
+ - '**'
+
+jobs:
+ build-server-musl:
+ name: build-server-musl
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: rustup toolchain install stable --profile minimal
+ - run: rustup target add x86_64-unknown-linux-musl --toolchain stable
+ - run: sudo apt install gcc musl-tools cmake clang
+ - uses: Swatinem/rust-cache@v2.7.0
+ with:
+ workspaces: "server"
+ save-if: ${{ github.ref == 'refs/heads/master' }}
+ - run: cargo build --release --target x86_64-unknown-linux-musl
+ working-directory: server
+ - name: Upload artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: server-x86_64-unknown-linux-musl
+ path: ./server/target/x86_64-unknown-linux-musl/release/digidecs
+
+ build-frontend:
+ name: build-frontend
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set Node.js 21
+ uses: actions/setup-node@v3
+ with:
+ node-version: 21.x
+ - name: Run install
+ uses: borales/actions-yarn@v4
+ with:
+ dir: frontend
+ cmd: install
+ - name: Build
+ uses: borales/actions-yarn@v4
+ with:
+ dir: frontend
+ cmd: build
+ - run: tar -czf frontend.tar.gz dist/*
+ working-directory: frontend
+ - name: Upload artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: frontend.tar.gz
+ path: ./frontend/frontend.tar.gz
+
+
+ create-release:
+ name: create-release
+ needs:
+ - build-server-musl
+ - build-frontend
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download all workflow run artifacts
+ uses: actions/download-artifact@v3
+
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: Release ${{ github.ref }}
+ draft: false
+
+ - run: mv server-x86_64-unknown-linux-musl/digidecs server-x86_64-unknown-linux-musl/server-x86_64-unknown-linux-musl
+
+ - name: Release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: |
+ server-x86_64-unknown-linux-musl/server-x86_64-unknown-linux-musl
+ frontend.tar.gz/frontend.tar.gz
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..508ee91
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,53 @@
+name: test
+on:
+ push: {}
+
+jobs:
+ server-fmt:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: rustup toolchain install stable --profile minimal
+ - run: rustup component add rustfmt
+ - uses: Swatinem/rust-cache@v2.7.0
+ with:
+ workspaces: "server"
+ save-if: ${{ github.ref == 'refs/heads/master' }}
+ - run: cargo fmt --all --check
+ working-directory: server
+
+ server-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: rustup toolchain install stable --profile minimal
+ - uses: Swatinem/rust-cache@v2.7.0
+ with:
+ workspaces: "server"
+ save-if: ${{ github.ref == 'refs/heads/master' }}
+ - run: cargo test
+ working-directory: server
+
+ server-clippy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run: rustup toolchain install stable --profile minimal
+ - run: rustup component add clippy
+ - uses: Swatinem/rust-cache@v2.7.0
+ with:
+ workspaces: "server"
+ save-if: ${{ github.ref == 'refs/heads/master' }}
+ - run: cargo clippy
+ working-directory: server
+
+ frontend-eslint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Install modules
+ working-directory: frontend
+ run: yarn
+ - name: Run eslint
+ working-directory: frontend
+ run: yarn eslint
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index 1745091..a530905 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,7 +3,7 @@
-
+
Sticky DigiDecs
diff --git a/frontend/src/plugins/locales/en.ts b/frontend/src/plugins/locales/en.ts
index a2c6b25..6166ac2 100644
--- a/frontend/src/plugins/locales/en.ts
+++ b/frontend/src/plugins/locales/en.ts
@@ -1,7 +1,45 @@
import {Locale} from "@/plugins/locales/locale";
const EN: Locale = {
-
+ site_title: "DigiDecs",
+ error: "Something went wrong, please try again later",
+ home: {
+ title: "DigiDecs",
+ subtitle: "Declare digitally at Sticky",
+ invalidFieldsError: "One or multiple fields are incorreect",
+ form: {
+ name: "Naam",
+ iban: "IBAN",
+ email: "Email",
+ value: "Amount",
+ what: "What",
+ commission: "For what / which commission",
+ notes: "Notes",
+ files: "Receipts / Invoices",
+ filesExplanation: "Only .pdf, .jpg or .png files. You can submit multiple documents. Ensure the date, the (VAT) amount and the different products or services are clearly readable.",
+ checked: "I have checked everything and filled this form thruthfully",
+ rules: {
+ required: "Required",
+ ibanInvalid: "Invalid IBAN",
+ emailInvalid: "Invalid email address",
+ valueInvalid: "Invalid amount",
+ filesTooLarge: "The maximum file size is 15MB"
+ },
+ hints: {
+ name: "Treasurer",
+ email: "{'eindbaas@svsticky.nl'}",
+ iban: "NL13TEST0123456789",
+ value: "19,19",
+ what: "Digging Machine",
+ commission: "The board, obviously!"
+ }
+ },
+ submit: "Submit"
+ },
+ submitted: {
+ title: 'Success!',
+ description: "Your digidecs has been sent! If you haven't received your money back after 7 days, contact the treasurer"
+ }
}
export default EN;
\ No newline at end of file
diff --git a/frontend/src/plugins/locales/locale.ts b/frontend/src/plugins/locales/locale.ts
index 8e2f59f..af73b46 100644
--- a/frontend/src/plugins/locales/locale.ts
+++ b/frontend/src/plugins/locales/locale.ts
@@ -1,11 +1,14 @@
export interface Locale {
site_title: string,
error: string,
+ submitted: {
+ title: string,
+ description: string
+ },
home: {
title: string,
subtitle: string,
invalidFieldsError: string,
- success: string,
form: {
name: string,
iban: string,
diff --git a/frontend/src/plugins/locales/nl.ts b/frontend/src/plugins/locales/nl.ts
index 6dbae80..553b83a 100644
--- a/frontend/src/plugins/locales/nl.ts
+++ b/frontend/src/plugins/locales/nl.ts
@@ -7,7 +7,6 @@ const NL: Locale = {
title: "DigiDecs",
subtitle: "Digitaal declareren bij Sticky",
invalidFieldsError: "Een of meerdere velden zijn onjuist ingevuld",
- success: "Success! Je declaratie is ingestuurd. Mocht je na 5 werkdagen je geldf nog niet hebben ontvangen, neem dan contact op met de Penningmeester: penningmeester@svsticky.nl.",
form: {
name: "Naam",
iban: "IBAN",
@@ -16,7 +15,7 @@ const NL: Locale = {
what: "Wat",
commission: "Waarvoor / Welke commissie",
notes: "Opmerkingen",
- files: "Bonnetjes",
+ files: "Bonnetjes / Facturen",
filesExplanation: "Alleen .pdf, .jpg, en .png bestanden, je kan meerdere bestanden selecteren. Zorg dat de datum, het (btw) bedrag en de verschillende producten of diensten goed leesbaar zijn.",
checked: "Ik heb alles gecheckt en naar waarheid ingevuld",
rules: {
@@ -36,6 +35,10 @@ const NL: Locale = {
}
},
submit: "Verstuur"
+ },
+ submitted: {
+ title: "Succes!",
+ description: "Jouw digidecs is verstuurd! Mocht je na 7 dagen je geld nog niet terug hebben gekregen, neem dan contact op met de penningmeester"
}
}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index bf044b4..60275ec 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -8,6 +8,10 @@ const routes: Array = [
{
path: '',
component: () => import('@/views/HomeView.vue')
+ },
+ {
+ path: 'complete',
+ component: () => import('@/views/SubmittedView.vue')
}
]
}
diff --git a/frontend/src/scripts/digidecs.ts b/frontend/src/scripts/digidecs.ts
index f8b6494..4a9c0df 100644
--- a/frontend/src/scripts/digidecs.ts
+++ b/frontend/src/scripts/digidecs.ts
@@ -2,10 +2,18 @@ import {Result} from "@/scripts/core/result";
import {ApiError} from "@/scripts/core/error";
import {fetch1} from "@/scripts/core/fetch1";
import {server} from "@/main";
-import {encode} from "base64-arraybuffer";
+
export class Digidecs {
- static async digidecs(
+ trackingId: string;
+ attachments: string[]
+
+ constructor(trackingId: string, attachments: string[]) {
+ this.trackingId = trackingId;
+ this.attachments = attachments;
+ }
+
+ static async start(
name: string,
iban: string,
email: string,
@@ -14,19 +22,8 @@ export class Digidecs {
commission: string,
notes: string,
attachments: File[],
- ): Promise> {
- const attachmentsBase64 = await Promise.all(attachments.map(async attachment => {
- const buffer = await attachment.arrayBuffer();
- // const content = btoa(String.fromCharCode(...new Uint8Array(buffer)));
- const content = encode(buffer);
- return {
- content: content,
- name: attachment.name,
- mime: attachment.type,
- }
- }));
-
- const r = await fetch1(`${server}/api/digidecs`, {
+ ): Promise> {
+ const r = await fetch1(`${server}/api/digidecs/start`, {
method: 'POST',
headers: {
'content-type': 'application/json',
@@ -39,14 +36,59 @@ export class Digidecs {
what: what,
commission: commission,
notes: notes,
- attachments: attachmentsBase64,
+ attachments: attachments.map((att) => {
+ return {
+ name: att.name,
+ mime: att.type,
+ };
+ }),
})
- });
+ })
+ if(r.isOk()) {
+ interface StartedDigidecsResponse {
+ tracking_id: string;
+ attachments: {
+ name: string,
+ mime: string,
+ tracking_id: string,
+ }[]
+ }
+
+
+ return r.map1(async (response) => {
+ const r = await response.json();
+ return new Digidecs(r.tracking_id, r.attachments.map((att) => att.tracking_id));
+ });
+ } else {
+ return Result.err(r.unwrapErr());
+ }
+ }
+
+ async upload_attachment(file: File, index: number): Promise> {
+ const r = await fetch1(`${server}/api/digidecs/attachment?tracking_id=${this.trackingId}&attachment_tracking_id=${this.attachments[index]}`, {
+ method: 'POST',
+ body: file,
+ });
+
+ if(r.isOk()) {
+ return Result.ok([]);
+ } else {
+ return Result.err(r.unwrapErr());
+ }
+ }
+
+ async complete(): Promise> {
+ const r = await fetch1(`${server}/api/digidecs/complete?tracking_id=${this.trackingId}`, {
+ method: 'POST',
+ });
+
if(r.isOk()) {
return Result.ok([]);
} else {
return Result.err(r.unwrapErr());
}
}
+
+
}
\ No newline at end of file
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
index 5cedb26..a40dcee 100644
--- a/frontend/src/views/HomeView.vue
+++ b/frontend/src/views/HomeView.vue
@@ -78,9 +78,9 @@
size
/>
-
+
{{ $t('home.form.filesExplanation') }}
-
+
+
+
+ {{ $t('submitted.title') }}
+
+
+ {{ $t('submitted.description') }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/Cargo.lock b/server/Cargo.lock
index 1bdd65d..bcf8ddd 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -606,6 +606,35 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "digidecs"
+version = "0.1.0"
+dependencies = [
+ "actix-cors",
+ "actix-route-config",
+ "actix-web",
+ "base64",
+ "clap",
+ "color-eyre",
+ "futures-util",
+ "handlebars",
+ "iban_validate",
+ "lettre",
+ "nix",
+ "noiseless-tracing-actix-web",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "time",
+ "tokio",
+ "tracing",
+ "tracing-actix-web",
+ "tracing-error",
+ "tracing-subscriber",
+]
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -1663,33 +1692,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "server"
-version = "0.1.0"
-dependencies = [
- "actix-cors",
- "actix-route-config",
- "actix-web",
- "base64",
- "clap",
- "color-eyre",
- "futures-util",
- "handlebars",
- "iban_validate",
- "lettre",
- "nix",
- "noiseless-tracing-actix-web",
- "regex",
- "serde",
- "serde_json",
- "thiserror",
- "tokio",
- "tracing",
- "tracing-actix-web",
- "tracing-error",
- "tracing-subscriber",
-]
-
[[package]]
name = "sha1"
version = "0.10.6"
diff --git a/server/Cargo.toml b/server/Cargo.toml
index dcf76e2..2206e11 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "server"
+name = "digidecs"
version = "0.1.0"
edition = "2021"
@@ -24,4 +24,6 @@ regex = "1.10.6"
iban_validate = "4.0.1"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing-error = "0.2.0"
-base64 = "0.22.1"
\ No newline at end of file
+base64 = "0.22.1"
+rand = "0.8.5"
+time = "0.3.36"
\ No newline at end of file
diff --git a/server/src/email/ipv4.rs b/server/src/email/ipv4.rs
index 80ac555..63ce7d3 100644
--- a/server/src/email/ipv4.rs
+++ b/server/src/email/ipv4.rs
@@ -22,16 +22,16 @@ pub async fn get_local_v4() -> color_eyre::Result {
80,
))),
)
- .await
+ .await
{
Ok(stream_r) => stream_r.map(|_| addr),
Err(e) => Err(std::io::Error::new(std::io::ErrorKind::TimedOut, e)),
}
}))
- .await
- .into_iter()
- .flatten()
- .collect::>();
+ .await
+ .into_iter()
+ .flatten()
+ .collect::>();
if connectable_addrs.is_empty() {
Err(color_eyre::eyre::Error::msg(
diff --git a/server/src/email/mod.rs b/server/src/email/mod.rs
index 675f0e2..b6285d9 100644
--- a/server/src/email/mod.rs
+++ b/server/src/email/mod.rs
@@ -1,18 +1,19 @@
use crate::file::SmtpConfig;
+use lettre::message::header::ContentType;
use lettre::message::{Mailbox, MultiPart, SinglePart};
use lettre::transport::smtp::client::{AsyncSmtpConnection, TlsParameters};
use lettre::transport::smtp::extension::ClientId;
use lettre::Address;
use lettre::Message;
+use rand::Rng;
use std::net::{IpAddr, Ipv4Addr};
use std::str::FromStr;
use std::time::Duration;
-use lettre::message::header::ContentType;
use thiserror::Error;
use tracing::{debug, error, trace};
-pub mod template;
pub mod ipv4;
+pub mod template;
#[derive(Debug, Error)]
pub enum SendError {
@@ -53,23 +54,28 @@ pub async fn send_email(
Address::from_str(&smtp_config.from_email)?,
);
- let mb_reply_to = Mailbox::new(
- Some(reply_to_name),
- Address::from_str(&reply_to_email)?,
- );
+ let mb_reply_to = Mailbox::new(Some(reply_to_name), Address::from_str(&reply_to_email)?);
let msg = Message::builder()
.reply_to(mb_reply_to)
.from(mb_from)
.to(mb_to)
- .subject(format!("[DigiDecs] Nieuwe declaratie: {commission}"));
+ .subject(format!(
+ "[DigiDecs] Nieuwe declaratie: {commission} ({})",
+ rand::thread_rng()
+ .sample_iter(rand::distributions::Alphanumeric)
+ .take(6)
+ .map(char::from)
+ .collect::()
+ ));
let mut mp = MultiPart::mixed().build();
mp = mp.singlepart(SinglePart::html(body));
for att in attachments {
- mp = mp.singlepart(lettre::message::Attachment::new(att.name)
- .body(att.content, ContentType::parse(&att.mime)?)
+ mp = mp.singlepart(
+ lettre::message::Attachment::new(att.name)
+ .body(att.content, ContentType::parse(&att.mime)?),
);
}
@@ -88,14 +94,14 @@ pub async fn send_email(
None,
Some(IpAddr::V4(local_addr4)),
)
- .await?;
+ .await?;
if conn.can_starttls() {
conn.starttls(
TlsParameters::new_rustls(smtp_config.smtp_relay.as_str().into())?,
&client_id,
)
- .await?;
+ .await?;
}
trace!("Checking SMTP connection");
diff --git a/server/src/email/template.rs b/server/src/email/template.rs
index 5a646ca..d0aeab1 100644
--- a/server/src/email/template.rs
+++ b/server/src/email/template.rs
@@ -14,9 +14,7 @@ pub struct TemplateData {
pub notes: Option,
}
-pub fn render_template(
- data: &TemplateData,
-) -> Result {
+pub fn render_template(data: &TemplateData) -> Result {
let mut engine = Handlebars::new();
engine.set_strict_mode(true);
@@ -24,4 +22,4 @@ pub fn render_template(
engine.set_dev_mode(true);
engine.render_template(TEMPLATE, data)
-}
\ No newline at end of file
+}
diff --git a/server/src/file/config.rs b/server/src/file/config.rs
index 8082c6e..3757760 100644
--- a/server/src/file/config.rs
+++ b/server/src/file/config.rs
@@ -1,11 +1,11 @@
-use serde::{Deserialize, Serialize};
use crate::file::DataFile;
+use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct AppConfig {
pub server: ServerConfig,
pub smtp: SmtpConfig,
- pub treasurer_email: String
+ pub treasurer_email: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -35,4 +35,4 @@ impl Default for ServerConfig {
}
}
-impl DataFile for AppConfig {}
\ No newline at end of file
+impl DataFile for AppConfig {}
diff --git a/server/src/server/mod.rs b/server/src/server/mod.rs
index 0fb6f91..e4a31db 100644
--- a/server/src/server/mod.rs
+++ b/server/src/server/mod.rs
@@ -1,24 +1,23 @@
-use actix_cors::Cors;
-use actix_route_config::Routable;
-use actix_web::{web, App, HttpServer};
-use noiseless_tracing_actix_web::NoiselessRootSpanBuilder;
-use tracing::info;
use crate::args::AppArgs;
use crate::email::ipv4::get_local_v4;
use crate::file::AppConfig;
use crate::server::types::{RuntimeData, WArgs, WConfig, WRuntime};
+use actix_cors::Cors;
+use actix_route_config::Routable;
+use actix_web::{App, HttpServer};
+use noiseless_tracing_actix_web::NoiselessRootSpanBuilder;
+use std::sync::{Arc, Mutex};
+use tracing::info;
-mod types;
mod routes;
+mod types;
-pub async fn run_server(
- config: AppConfig,
- args: AppArgs,
-) -> color_eyre::Result<()> {
+pub async fn run_server(config: AppConfig, args: AppArgs) -> color_eyre::Result<()> {
let port = config.server.port;
let runtime_data = RuntimeData {
local_v4_addr: get_local_v4().await?,
+ pending_digidecs: Arc::new(Mutex::new(vec![])),
};
info!("Using {} for SMTP connections", runtime_data.local_v4_addr);
@@ -31,15 +30,12 @@ pub async fn run_server(
.app_data(WConfig::new(config.clone()))
.app_data(WArgs::new(args.clone()))
.app_data(WRuntime::new(runtime_data.clone()))
- .app_data(web::JsonConfig::default()
- .limit(20*10^6)
- )
.configure(routes::Router::configure)
})
- .bind(format!("0.0.0.0:{port}"))?
- .server_hostname(&host)
- .run()
- .await?;
+ .bind(format!("0.0.0.0:{port}"))?
+ .server_hostname(&host)
+ .run()
+ .await?;
Ok(())
-}
\ No newline at end of file
+}
diff --git a/server/src/server/routes/digidecs.rs b/server/src/server/routes/digidecs.rs
deleted file mode 100644
index 0af22b4..0000000
--- a/server/src/server/routes/digidecs.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-use crate::email::send_email;
-use crate::email::template::{render_template, TemplateData};
-use crate::server::types::{Empty, Error, WArgs, WConfig, WResult, WRuntime};
-use actix_web::web;
-use iban::Iban;
-use regex::Regex;
-use serde::Deserialize;
-use std::str::FromStr;
-use std::sync::OnceLock;
-use base64::{DecodeError, Engine};
-use tracing::{info, instrument};
-
-#[derive(Deserialize)]
-pub struct DigidecsRequest {
- pub name: String,
- pub iban: String,
- pub email: String,
- pub value: f64,
- pub what: String,
- pub commission: String,
- pub notes: Option,
- pub attachments: Vec,
-}
-
-#[derive(Deserialize)]
-pub struct Attachment {
- pub content: String,
- pub name: String,
- pub mime: String,
-}
-
-#[instrument(skip_all)]
-pub async fn digidecs(
- config: WConfig,
- runtime_config: WRuntime,
- payload: web::Json,
- args: WArgs,
-) -> WResult {
- let payload = payload.into_inner();
-
- if !validate_email(&payload.email) {
- return Err(Error::InvalidEmail);
- }
-
- if !validate_iban(&payload.iban) {
- return Err(Error::InvalidIban);
- }
-
- if payload.attachments.is_empty() {
- return Err(Error::MissingAttachment);
- }
-
- if payload.value <= 0.0 {
- return Err(Error::ValueNegativeOrZero);
-
- }
-
- let attachments = payload.attachments.into_iter()
- .map(|att| {
- Ok(crate::email::Attachment {
- content: base64::engine::general_purpose::STANDARD.decode(&att.content.as_bytes())?,
- name: att.name,
- mime: att.mime
- })
- })
- .collect::, DecodeError>>()?;
-
- let email_body = render_template(&TemplateData {
- name: payload.name.clone(),
- iban: payload.iban,
- email: payload.email.clone(),
- value: format!("{:.2}", payload.value),
- what: payload.what.clone(),
- commission: payload.commission,
- notes: payload.notes,
- })?;
-
- if args.dry_run {
- info!("Dry run is enabled. Not sending email.");
- info!("Email body: \n{email_body}");
- } else {
- send_email(
- &config.smtp,
- runtime_config.local_v4_addr,
- &config.treasurer_email,
- email_body,
- payload.name,
- &payload.email,
- &payload.what,
- attachments,
- ).await?;
- }
-
-
-
- Ok(Empty)
-}
-
-static EMAIL_REGEX: OnceLock = OnceLock::new();
-
-fn validate_email(email: &str) -> bool {
- let regex = EMAIL_REGEX.get_or_init(|| Regex::new(r#"[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+"#).unwrap());
- regex.is_match(email)
-}
-
-fn validate_iban(iban: &str) -> bool {
- Iban::from_str(iban).is_ok()
-}
\ No newline at end of file
diff --git a/server/src/server/routes/digidecs/attachment.rs b/server/src/server/routes/digidecs/attachment.rs
new file mode 100644
index 0000000..eccfb2b
--- /dev/null
+++ b/server/src/server/routes/digidecs/attachment.rs
@@ -0,0 +1,34 @@
+use actix_web::web;
+use serde::Deserialize;
+use tracing::instrument;
+
+use crate::server::types::{Empty, Error, WResult, WRuntime};
+
+#[derive(Deserialize)]
+pub struct Query {
+ tracking_id: String,
+ attachment_tracking_id: String,
+}
+
+#[instrument(skip_all)]
+pub async fn attachment(
+ query: web::Query,
+ payload: web::Bytes,
+ runtime: WRuntime,
+) -> WResult {
+ let mut lock = runtime.pending_digidecs.lock().unwrap();
+ let digidecs = lock
+ .iter_mut()
+ .find(|digidecs| digidecs.tracking_id.eq(&query.tracking_id))
+ .ok_or(Error::UnknownTrackingId)?;
+
+ let attachment = digidecs
+ .attachments
+ .iter_mut()
+ .find(|att| att.tracking_id.eq(&query.attachment_tracking_id))
+ .ok_or(Error::UnknownAttachmentTrackingId)?;
+
+ attachment.content = Some(payload.to_vec());
+
+ Ok(Empty)
+}
diff --git a/server/src/server/routes/digidecs/complete.rs b/server/src/server/routes/digidecs/complete.rs
new file mode 100644
index 0000000..93c5ebd
--- /dev/null
+++ b/server/src/server/routes/digidecs/complete.rs
@@ -0,0 +1,83 @@
+use crate::email::send_email;
+use crate::email::template::{render_template, TemplateData};
+use crate::server::types::{Empty, Error, WArgs, WConfig, WResult, WRuntime};
+use actix_web::web;
+use serde::Deserialize;
+use time::OffsetDateTime;
+use tracing::{info, instrument, trace};
+
+#[derive(Deserialize)]
+pub struct Query {
+ tracking_id: String,
+}
+
+#[instrument(skip_all)]
+pub async fn complete(
+ query: web::Query,
+ config: WConfig,
+ runtime: WRuntime,
+ args: WArgs,
+) -> WResult {
+ let mut lock = runtime.pending_digidecs.lock().unwrap();
+ let (idx, _) = lock
+ .iter()
+ .enumerate()
+ .find(|(_, digidecs)| digidecs.tracking_id.eq(&query.tracking_id))
+ .ok_or(Error::UnknownTrackingId)?;
+
+ let digidecs = lock.remove(idx);
+
+ if digidecs.expires_at <= OffsetDateTime::now_utc() {
+ return Err(Error::DigidecsExpired);
+ }
+
+ let attachments_cnt = digidecs
+ .attachments
+ .iter()
+ .filter(|att| att.content.is_some())
+ .count();
+
+ if digidecs.attachment_count != attachments_cnt {
+ return Err(Error::MissingAttachment);
+ }
+
+ let email_body = render_template(&TemplateData {
+ name: digidecs.data.name.clone(),
+ iban: digidecs.data.iban.clone(),
+ email: digidecs.data.email.clone(),
+ value: format!("{:.2}", digidecs.data.value),
+ what: digidecs.data.what.clone(),
+ commission: digidecs.data.commission.clone(),
+ notes: digidecs.data.notes.clone(),
+ })?;
+
+ let attachments = digidecs
+ .attachments
+ .into_iter()
+ .map(|att| crate::email::Attachment {
+ name: att.name,
+ mime: att.mime,
+ content: att.content.unwrap(), // Some state is checked when checking if every attachment has content set earier
+ })
+ .collect::>();
+
+ if args.dry_run {
+ info!("Dry run is enabled. Not sending email.");
+ info!("Email body: \n{email_body}");
+ } else {
+ trace!("Sending Digidecs email");
+ send_email(
+ &config.smtp,
+ runtime.local_v4_addr,
+ &config.treasurer_email,
+ email_body,
+ digidecs.data.name.clone(),
+ &digidecs.data.email.clone(),
+ &digidecs.data.what.clone(),
+ attachments,
+ )
+ .await?;
+ }
+
+ Ok(Empty)
+}
diff --git a/server/src/server/routes/digidecs/mod.rs b/server/src/server/routes/digidecs/mod.rs
new file mode 100644
index 0000000..3eb9e03
--- /dev/null
+++ b/server/src/server/routes/digidecs/mod.rs
@@ -0,0 +1,20 @@
+use actix_route_config::Routable;
+use actix_web::web;
+use actix_web::web::ServiceConfig;
+
+mod attachment;
+mod complete;
+mod start;
+
+pub struct Router;
+
+impl Routable for Router {
+ fn configure(config: &mut ServiceConfig) {
+ config.service(
+ web::scope("/digidecs")
+ .route("/start", web::post().to(start::start))
+ .route("/attachment", web::post().to(attachment::attachment))
+ .route("/complete", web::post().to(complete::complete)),
+ );
+ }
+}
diff --git a/server/src/server/routes/digidecs/start.rs b/server/src/server/routes/digidecs/start.rs
new file mode 100644
index 0000000..65fa194
--- /dev/null
+++ b/server/src/server/routes/digidecs/start.rs
@@ -0,0 +1,130 @@
+use std::str::FromStr;
+use std::sync::OnceLock;
+
+use actix_web::web;
+use iban::Iban;
+use rand::Rng;
+use regex::Regex;
+use serde::{Deserialize, Serialize};
+use time::{Duration, OffsetDateTime};
+use tracing::instrument;
+
+use crate::server::types::{
+ Error, PendingDigidecs, PendingDigidecsAttachment, PendingDigidecsData, WResult, WRuntime,
+};
+
+#[derive(Deserialize)]
+pub struct StartDigidecsRequest {
+ name: String,
+ iban: String,
+ email: String,
+ value: f64,
+ what: String,
+ commission: String,
+ notes: Option,
+ attachments: Vec,
+}
+
+#[derive(Deserialize)]
+pub struct Attachment {
+ name: String,
+ mime: String,
+}
+
+#[derive(Serialize)]
+pub struct StartDigidecsResponse {
+ tracking_id: String,
+ attachments: Vec,
+}
+
+#[derive(Serialize)]
+pub struct AttachmentResponse {
+ name: String,
+ mime: String,
+ tracking_id: String,
+}
+
+#[instrument(skip_all)]
+pub async fn start(
+ payload: web::Json,
+ runtime: WRuntime,
+) -> WResult> {
+ let payload = payload.into_inner();
+
+ if !validate_email(&payload.email) {
+ return Err(Error::InvalidEmail);
+ }
+
+ if !validate_iban(&payload.iban) {
+ return Err(Error::InvalidIban);
+ }
+
+ if payload.attachments.is_empty() {
+ return Err(Error::MissingAttachment);
+ }
+
+ if payload.value <= 0.0 {
+ return Err(Error::ValueNegativeOrZero);
+ }
+
+ let tracking_id = gen_tracking_id();
+ let attachment_tracking_ids = payload
+ .attachments
+ .iter()
+ .map(|att| AttachmentResponse {
+ name: att.name.clone(),
+ tracking_id: gen_tracking_id(),
+ mime: att.mime.clone(),
+ })
+ .collect::>();
+
+ let mut lock = runtime.pending_digidecs.lock().unwrap();
+ lock.push(PendingDigidecs {
+ expires_at: OffsetDateTime::now_utc() + Duration::hours(1),
+ tracking_id: tracking_id.clone(),
+ attachment_count: attachment_tracking_ids.len(),
+ attachments: attachment_tracking_ids
+ .iter()
+ .map(|att| PendingDigidecsAttachment {
+ name: att.name.clone(),
+ tracking_id: att.tracking_id.clone(),
+ content: None,
+ mime: att.mime.clone(),
+ })
+ .collect(),
+ data: PendingDigidecsData {
+ name: payload.name,
+ email: payload.email,
+ value: payload.value,
+ iban: payload.iban,
+ notes: payload.notes,
+ what: payload.what,
+ commission: payload.commission,
+ },
+ });
+
+ Ok(web::Json(StartDigidecsResponse {
+ tracking_id,
+ attachments: attachment_tracking_ids,
+ }))
+}
+
+static EMAIL_REGEX: OnceLock = OnceLock::new();
+
+fn gen_tracking_id() -> String {
+ rand::thread_rng()
+ .sample_iter(rand::distributions::Alphanumeric)
+ .take(16)
+ .map(char::from)
+ .collect()
+}
+
+fn validate_email(email: &str) -> bool {
+ let regex = EMAIL_REGEX
+ .get_or_init(|| Regex::new(r#"[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+"#).unwrap());
+ regex.is_match(email)
+}
+
+fn validate_iban(iban: &str) -> bool {
+ Iban::from_str(iban).is_ok()
+}
diff --git a/server/src/server/routes/mod.rs b/server/src/server/routes/mod.rs
index 8fbd233..6a02a72 100644
--- a/server/src/server/routes/mod.rs
+++ b/server/src/server/routes/mod.rs
@@ -8,8 +8,6 @@ pub struct Router;
impl Routable for Router {
fn configure(config: &mut ServiceConfig) {
- config.service(web::scope("/api")
- .route("/digidecs", web::post().to(digidecs::digidecs))
- );
+ config.service(web::scope("/api").configure(digidecs::Router::configure));
}
-}
\ No newline at end of file
+}
diff --git a/server/src/server/types/data.rs b/server/src/server/types/data.rs
index 488900c..80ae270 100644
--- a/server/src/server/types/data.rs
+++ b/server/src/server/types/data.rs
@@ -2,6 +2,8 @@ use crate::args::AppArgs;
use crate::file::AppConfig;
use actix_web::web;
use std::net::Ipv4Addr;
+use std::sync::{Arc, Mutex};
+use time::OffsetDateTime;
pub type WConfig = web::Data;
@@ -12,4 +14,33 @@ pub type WRuntime = web::Data;
#[derive(Clone)]
pub struct RuntimeData {
pub local_v4_addr: Ipv4Addr,
+ pub pending_digidecs: Arc>>,
+}
+
+#[derive(Clone)]
+pub struct PendingDigidecs {
+ pub expires_at: OffsetDateTime,
+ pub data: PendingDigidecsData,
+ pub tracking_id: String,
+ pub attachment_count: usize,
+ pub attachments: Vec,
+}
+
+#[derive(Clone)]
+pub struct PendingDigidecsData {
+ pub name: String,
+ pub iban: String,
+ pub email: String,
+ pub value: f64,
+ pub what: String,
+ pub commission: String,
+ pub notes: Option,
+}
+
+#[derive(Clone)]
+pub struct PendingDigidecsAttachment {
+ pub name: String,
+ pub tracking_id: String,
+ pub mime: String,
+ pub content: Option>,
}
diff --git a/server/src/server/types/error.rs b/server/src/server/types/error.rs
index fa5e85f..1a85ff6 100644
--- a/server/src/server/types/error.rs
+++ b/server/src/server/types/error.rs
@@ -14,12 +14,18 @@ pub enum Error {
InvalidIban,
#[error("Invalid Email address")]
InvalidEmail,
- #[error("At least one attachment is required")]
+ #[error("Missing attachments")]
MissingAttachment,
#[error("Value may not be negative or zero")]
ValueNegativeOrZero,
#[error("Attachment contains invalid base64")]
InvalidAttachmentBase64(#[from] base64::DecodeError),
+ #[error("No digidecs with that tracking ID exists")]
+ UnknownTrackingId,
+ #[error("No digidecs attachment with that tracking ID exists")]
+ UnknownAttachmentTrackingId,
+ #[error("Digidecs has expired. Start over again")]
+ DigidecsExpired,
}
impl ResponseError for Error {
@@ -32,6 +38,9 @@ impl ResponseError for Error {
Self::MissingAttachment => StatusCode::BAD_REQUEST,
Self::ValueNegativeOrZero => StatusCode::BAD_REQUEST,
Self::InvalidAttachmentBase64(_) => StatusCode::BAD_REQUEST,
+ Self::UnknownTrackingId => StatusCode::NOT_FOUND,
+ Self::UnknownAttachmentTrackingId => StatusCode::NOT_FOUND,
+ Self::DigidecsExpired => StatusCode::BAD_REQUEST,
}
}
}
diff --git a/server/src/server/types/mod.rs b/server/src/server/types/mod.rs
index 035b23e..fba3789 100644
--- a/server/src/server/types/mod.rs
+++ b/server/src/server/types/mod.rs
@@ -4,4 +4,4 @@ pub mod error;
pub use data::*;
pub use empty::*;
-pub use error::*;
\ No newline at end of file
+pub use error::*;