mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-19 16:21:30 +01:00
email wip
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,7 @@
|
||||
.devenv
|
||||
database
|
||||
node_modules
|
||||
|
||||
config.yaml
|
||||
flake.nix
|
||||
|
||||
|
||||
|
||||
203
Cargo.lock
generated
203
Cargo.lock
generated
@@ -240,6 +240,12 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -899,6 +905,9 @@ name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
@@ -1227,18 +1236,66 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d84f8d224ac58107d53d3ec2b9ad39fd8c8c4e285d3c9cb35485ffd2ca88cb3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"convert_case",
|
||||
"json5",
|
||||
"pathdiff",
|
||||
"ron",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
"winnow",
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
@@ -1533,6 +1590,15 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||
dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dmp"
|
||||
version = "0.2.0"
|
||||
@@ -2061,6 +2127,15 @@ version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.8.0"
|
||||
@@ -2550,6 +2625,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json5"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "9.3.0"
|
||||
@@ -3238,6 +3324,16 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -3318,6 +3414,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -3366,6 +3468,40 @@ dependencies = [
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
@@ -4140,6 +4276,18 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30"
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bitflags 2.6.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstar"
|
||||
version = "0.12.0"
|
||||
@@ -4151,6 +4299,17 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
"trim-in-place",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
@@ -4496,6 +4655,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -5247,11 +5415,26 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
@@ -5260,6 +5443,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
dependencies = [
|
||||
"indexmap 2.6.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
@@ -5405,6 +5590,12 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trim-in-place"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -6065,6 +6256,17 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d"
|
||||
dependencies = [
|
||||
"arraydeque",
|
||||
"encoding_rs",
|
||||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
@@ -6171,6 +6373,7 @@ dependencies = [
|
||||
"axum_session_auth",
|
||||
"axum_session_surreal",
|
||||
"axum_typed_multipart",
|
||||
"config",
|
||||
"futures",
|
||||
"lapin",
|
||||
"lettre",
|
||||
|
||||
@@ -12,6 +12,7 @@ axum_session = "0.14.4"
|
||||
axum_session_auth = "0.14.1"
|
||||
axum_session_surreal = "0.2.1"
|
||||
axum_typed_multipart = "0.12.1"
|
||||
config = "0.15.4"
|
||||
futures = "0.3.31"
|
||||
lapin = { version = "2.5.0", features = ["serde_json"] }
|
||||
lettre = { version = "0.11.11", features = ["rustls-tls"] }
|
||||
|
||||
1
assets/htmx.min.js
vendored
Normal file
1
assets/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
assets/manifest.json
Normal file
13
assets/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "minne",
|
||||
"short_name": "minne",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
227
assets/style.css
227
assets/style.css
@@ -877,6 +877,57 @@ html {
|
||||
content: var(--tw-content);
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--rounded-box, 1rem);
|
||||
}
|
||||
|
||||
.card:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.card figure {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card.image-full {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.card.image-full:before {
|
||||
position: relative;
|
||||
content: "";
|
||||
z-index: 10;
|
||||
border-radius: var(--rounded-box, 1rem);
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.card.image-full:before,
|
||||
.card.image-full > * {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
}
|
||||
|
||||
.card.image-full > figure img {
|
||||
height: 100%;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card.image-full > .card-body {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
--tw-text-opacity: 1;
|
||||
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -1164,6 +1215,34 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-auto-flow: row;
|
||||
place-items: start;
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
row-gap: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
display: grid;
|
||||
place-items: start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.footer {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
.footer-center {
|
||||
grid-auto-flow: row dense;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1182,20 +1261,6 @@ html {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item) {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex-shrink: 1;
|
||||
-webkit-appearance: none;
|
||||
@@ -1514,6 +1579,44 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
.card :where(figure:first-child) {
|
||||
overflow: hidden;
|
||||
border-start-start-radius: inherit;
|
||||
border-start-end-radius: inherit;
|
||||
border-end-start-radius: unset;
|
||||
border-end-end-radius: unset;
|
||||
}
|
||||
|
||||
.card :where(figure:last-child) {
|
||||
overflow: hidden;
|
||||
border-start-start-radius: unset;
|
||||
border-start-end-radius: unset;
|
||||
border-end-start-radius: inherit;
|
||||
border-end-end-radius: inherit;
|
||||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.card.bordered {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
|
||||
}
|
||||
|
||||
.card.compact .card-body {
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.card.image-full :where(figure) {
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.checkbox:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1916,78 +2019,6 @@ html {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item) {
|
||||
bottom: auto;
|
||||
inset-inline-end: 0px;
|
||||
inset-inline-start: auto;
|
||||
top: 0px;
|
||||
--tw-translate-y: -50%;
|
||||
--tw-translate-x: 50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item):where([dir="rtl"], [dir="rtl"] *) {
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-start) {
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 0px;
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-start):where([dir="rtl"], [dir="rtl"] *) {
|
||||
--tw-translate-x: 50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-center) {
|
||||
inset-inline-end: 50%;
|
||||
inset-inline-start: 50%;
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-center):where([dir="rtl"], [dir="rtl"] *) {
|
||||
--tw-translate-x: 50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-end) {
|
||||
inset-inline-end: 0px;
|
||||
inset-inline-start: auto;
|
||||
--tw-translate-x: 50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-end):where([dir="rtl"], [dir="rtl"] *) {
|
||||
--tw-translate-x: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-bottom) {
|
||||
bottom: 0px;
|
||||
top: auto;
|
||||
--tw-translate-y: 50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-middle) {
|
||||
bottom: 50%;
|
||||
top: 50%;
|
||||
--tw-translate-y: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.indicator :where(.indicator-item.indicator-top) {
|
||||
bottom: auto;
|
||||
top: 0px;
|
||||
--tw-translate-y: -50%;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.menu-horizontal {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
@@ -2027,10 +2058,6 @@ html {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -2068,10 +2095,6 @@ html {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.max-h-full {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.min-h-\[80vh\] {
|
||||
min-height: 80vh;
|
||||
}
|
||||
@@ -2080,10 +2103,6 @@ html {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.min-h-full {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -2092,14 +2111,14 @@ html {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.max-w-lg {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
@@ -2159,6 +2178,10 @@ html {
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use zettle_db::{
|
||||
AppState,
|
||||
},
|
||||
storage::{db::SurrealDbClient, types::user::User},
|
||||
utils::mailer::Mailer,
|
||||
utils::{config::get_config, mailer::Mailer},
|
||||
};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -44,12 +44,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.try_init()
|
||||
.ok();
|
||||
|
||||
let config = get_config()?;
|
||||
|
||||
info!("{:?}", config);
|
||||
|
||||
// Set up RabbitMQ
|
||||
let config = RabbitMQConfig {
|
||||
amqp_addr: "amqp://localhost".to_string(),
|
||||
exchange: "my_exchange".to_string(),
|
||||
queue: "my_queue".to_string(),
|
||||
routing_key: "my_key".to_string(),
|
||||
let rabbitmq_config = RabbitMQConfig {
|
||||
amqp_addr: config.rabbitmq_address,
|
||||
exchange: config.rabbitmq_exchange,
|
||||
queue: config.rabbitmq_queue,
|
||||
routing_key: config.rabbitmq_routing_key,
|
||||
};
|
||||
|
||||
let reloader = AutoReloader::new(move |notifier| {
|
||||
@@ -62,14 +66,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(env)
|
||||
});
|
||||
|
||||
let mailer = Mailer::new();
|
||||
|
||||
let app_state = AppState {
|
||||
rabbitmq_producer: Arc::new(RabbitMQProducer::new(&config).await?),
|
||||
rabbitmq_consumer: Arc::new(RabbitMQConsumer::new(&config, false).await?),
|
||||
surreal_db_client: Arc::new(SurrealDbClient::new().await?),
|
||||
rabbitmq_producer: Arc::new(RabbitMQProducer::new(&rabbitmq_config).await?),
|
||||
rabbitmq_consumer: Arc::new(RabbitMQConsumer::new(&rabbitmq_config, false).await?),
|
||||
surreal_db_client: Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
openai_client: Arc::new(async_openai::Client::new()),
|
||||
templates: Arc::new(reloader),
|
||||
mailer: Arc::new(Mailer::new(
|
||||
config.smtp_username,
|
||||
config.smtp_relayer,
|
||||
config.smtp_password,
|
||||
)?),
|
||||
};
|
||||
|
||||
// setup_auth(&app_state.surreal_db_client).await?;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use tracing::info;
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
use zettle_db::rabbitmq::{consumer::RabbitMQConsumer, RabbitMQConfig};
|
||||
use zettle_db::{
|
||||
ingress::content_processor::ContentProcessor,
|
||||
rabbitmq::{consumer::RabbitMQConsumer, RabbitMQConfig, RabbitMQError},
|
||||
utils::config::get_config,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -13,19 +17,47 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
info!("Starting RabbitMQ consumer");
|
||||
|
||||
let config = get_config()?;
|
||||
|
||||
// Set up RabbitMQ config
|
||||
let config = RabbitMQConfig {
|
||||
amqp_addr: "amqp://localhost".to_string(),
|
||||
exchange: "my_exchange".to_string(),
|
||||
queue: "my_queue".to_string(),
|
||||
routing_key: "my_key".to_string(),
|
||||
let rabbitmq_config = RabbitMQConfig {
|
||||
amqp_addr: config.rabbitmq_address.clone(),
|
||||
exchange: config.rabbitmq_exchange.clone(),
|
||||
queue: config.rabbitmq_queue.clone(),
|
||||
routing_key: config.rabbitmq_routing_key.clone(),
|
||||
};
|
||||
|
||||
// Create a RabbitMQ consumer
|
||||
let consumer = RabbitMQConsumer::new(&config, true).await?;
|
||||
let consumer = RabbitMQConsumer::new(&rabbitmq_config, true).await?;
|
||||
|
||||
// Start consuming messages
|
||||
consumer.process_messages().await?;
|
||||
loop {
|
||||
match consumer.consume().await {
|
||||
Ok((ingress, delivery)) => {
|
||||
info!("Received IngressObject: {:?}", ingress);
|
||||
// Get the TextContent
|
||||
let text_content = ingress.to_text_content().await?;
|
||||
|
||||
// Initialize ContentProcessor which handles LLM analysis and storage
|
||||
let content_processor = ContentProcessor::new(&config).await?;
|
||||
|
||||
// Begin processing of TextContent
|
||||
content_processor.process(&text_content).await?;
|
||||
|
||||
// Remove from queue
|
||||
consumer.ack_delivery(delivery).await?;
|
||||
}
|
||||
Err(RabbitMQError::ConsumeError(e)) => {
|
||||
error!("Error consuming message: {}", e);
|
||||
// Optionally add a delay before trying again
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unexpected error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use tokio::task::JoinError;
|
||||
|
||||
use crate::{
|
||||
ingress::types::ingress_input::IngressContentError, rabbitmq::RabbitMQError,
|
||||
storage::types::file_info::FileError,
|
||||
storage::types::file_info::FileError, utils::mailer::EmailError,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -70,6 +70,8 @@ pub enum ApiError {
|
||||
AuthRequired,
|
||||
#[error("Templating error: {0}")]
|
||||
TemplatingError(#[from] minijinja::Error),
|
||||
#[error("Mail error: {0}")]
|
||||
EmailError(#[from] EmailError),
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
@@ -90,6 +92,7 @@ impl IntoResponse for ApiError {
|
||||
ApiError::RabbitMQError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
ApiError::FileError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
ApiError::TemplatingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
ApiError::EmailError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
};
|
||||
|
||||
(
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
text_chunk::TextChunk, text_content::TextContent,
|
||||
},
|
||||
},
|
||||
utils::embedding::generate_embedding,
|
||||
utils::{config::AppConfig, embedding::generate_embedding},
|
||||
};
|
||||
|
||||
use super::analysis::{
|
||||
@@ -25,9 +25,16 @@ pub struct ContentProcessor {
|
||||
}
|
||||
|
||||
impl ContentProcessor {
|
||||
pub async fn new() -> Result<Self, ProcessingError> {
|
||||
pub async fn new(app_config: &AppConfig) -> Result<Self, ProcessingError> {
|
||||
Ok(Self {
|
||||
db_client: SurrealDbClient::new().await?,
|
||||
db_client: SurrealDbClient::new(
|
||||
&app_config.surrealdb_address,
|
||||
&app_config.surrealdb_username,
|
||||
&app_config.surrealdb_password,
|
||||
&app_config.surrealdb_namespace,
|
||||
&app_config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
openai_client: async_openai::Client::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use futures::StreamExt;
|
||||
use lapin::{message::Delivery, options::*, types::FieldTable, Channel, Consumer, Queue};
|
||||
|
||||
use crate::{
|
||||
error::IngressConsumerError,
|
||||
ingress::{content_processor::ContentProcessor, types::ingress_object::IngressObject},
|
||||
};
|
||||
use crate::ingress::types::ingress_object::IngressObject;
|
||||
|
||||
use super::{RabbitMQCommon, RabbitMQCommonTrait, RabbitMQConfig, RabbitMQError};
|
||||
use tracing::{error, info};
|
||||
|
||||
/// Struct to consume messages from RabbitMQ.
|
||||
pub struct RabbitMQConsumer {
|
||||
@@ -193,38 +189,6 @@ impl RabbitMQConsumer {
|
||||
.await
|
||||
.map_err(|e| RabbitMQError::ConsumeError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Function to continually consume messages as they come in
|
||||
pub async fn process_messages(&self) -> Result<(), IngressConsumerError> {
|
||||
loop {
|
||||
match self.consume().await {
|
||||
Ok((ingress, delivery)) => {
|
||||
info!("Received IngressObject: {:?}", ingress);
|
||||
// Get the TextContent
|
||||
let text_content = ingress.to_text_content().await?;
|
||||
|
||||
// Initialize ContentProcessor which handles LLM analysis and storage
|
||||
let content_processor = ContentProcessor::new().await?;
|
||||
|
||||
// Begin processing of TextContent
|
||||
content_processor.process(&text_content).await?;
|
||||
|
||||
// Remove from queue
|
||||
self.ack_delivery(delivery).await?;
|
||||
}
|
||||
Err(RabbitMQError::ConsumeError(e)) => {
|
||||
error!("Error consuming message: {}", e);
|
||||
// Optionally add a delay before trying again
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unexpected error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::rabbitmq::consumer::RabbitMQConsumer;
|
||||
use crate::rabbitmq::publisher::RabbitMQProducer;
|
||||
use crate::storage::db::SurrealDbClient;
|
||||
use crate::utils::mailer::Mailer;
|
||||
use minijinja_autoreload::AutoReloader;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -14,4 +15,5 @@ pub struct AppState {
|
||||
pub surreal_db_client: Arc<SurrealDbClient>,
|
||||
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
|
||||
pub templates: Arc<AutoReloader>,
|
||||
pub mailer: Arc<Mailer>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse};
|
||||
use tracing::info;
|
||||
use minijinja::context;
|
||||
use tracing::{info, Instrument};
|
||||
|
||||
use crate::{error::ApiError, server::AppState};
|
||||
|
||||
@@ -12,6 +13,10 @@ pub async fn queue_length_handler(
|
||||
|
||||
info!("Queue length: {}", queue_length);
|
||||
|
||||
state
|
||||
.mailer
|
||||
.send_email_verification("per@starks.cloud", "1001010", &state.templates)?;
|
||||
|
||||
// Return the queue length with a 200 OK status
|
||||
Ok((StatusCode::OK, queue_length.to_string()))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
storage::types::user::User,
|
||||
};
|
||||
|
||||
page_data!(IndexData, {
|
||||
page_data!(IndexData, "index.html", {
|
||||
queue_length: u32,
|
||||
});
|
||||
|
||||
@@ -25,7 +25,9 @@ pub async fn index_handler(
|
||||
|
||||
let queue_length = state.rabbitmq_consumer.get_queue_length().await?;
|
||||
|
||||
let output = render_template("index.html", IndexData { queue_length }, state.templates)?;
|
||||
let data = IndexData { queue_length };
|
||||
|
||||
let output = render_template(IndexData::template_name(), data, state.templates)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ pub mod auth;
|
||||
pub mod index;
|
||||
pub mod search_result;
|
||||
|
||||
pub trait PageData {
|
||||
fn template_name() -> &'static str;
|
||||
}
|
||||
|
||||
pub fn render_template<T>(
|
||||
template_name: &str,
|
||||
context: T,
|
||||
@@ -44,12 +48,19 @@ where
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! page_data {
|
||||
($name:ident, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
|
||||
($name:ident, $template_name:expr, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
|
||||
use serde::{Serialize, Deserialize};
|
||||
use $crate::server::routes::html::PageData;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct $name {
|
||||
$($(#[$attr])* pub $field: $ty),*
|
||||
}
|
||||
|
||||
impl PageData for $name {
|
||||
fn template_name() -> &'static str {
|
||||
$template_name
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,18 +18,20 @@ impl SurrealDbClient {
|
||||
///
|
||||
/// # Returns
|
||||
/// * `SurrealDbClient` initialized
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
let db = connect("ws://127.0.0.1:8000").await?;
|
||||
pub async fn new(
|
||||
address: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
namespace: &str,
|
||||
database: &str,
|
||||
) -> Result<Self, Error> {
|
||||
let db = connect(address).await?;
|
||||
|
||||
// Sign in to database
|
||||
db.signin(Root {
|
||||
username: "root_user",
|
||||
password: "root_password",
|
||||
})
|
||||
.await?;
|
||||
db.signin(Root { username, password }).await?;
|
||||
|
||||
// Set namespace
|
||||
db.use_ns("test").use_db("test").await?;
|
||||
db.use_ns(namespace).use_db(database).await?;
|
||||
|
||||
Ok(SurrealDbClient { client: db })
|
||||
}
|
||||
|
||||
38
src/utils/config.rs
Normal file
38
src/utils/config.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use config::{Config, ConfigError, File};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppConfig {
|
||||
pub smtp_username: String,
|
||||
pub smtp_password: String,
|
||||
pub smtp_relayer: String,
|
||||
pub rabbitmq_address: String,
|
||||
pub rabbitmq_exchange: String,
|
||||
pub rabbitmq_queue: String,
|
||||
pub rabbitmq_routing_key: String,
|
||||
pub surrealdb_address: String,
|
||||
pub surrealdb_username: String,
|
||||
pub surrealdb_password: String,
|
||||
pub surrealdb_namespace: String,
|
||||
pub surrealdb_database: String,
|
||||
}
|
||||
|
||||
pub fn get_config() -> Result<AppConfig, ConfigError> {
|
||||
let config = Config::builder()
|
||||
.add_source(File::with_name("config"))
|
||||
.build()?;
|
||||
|
||||
Ok(AppConfig {
|
||||
smtp_username: config.get_string("SMTP_USERNAME")?,
|
||||
smtp_password: config.get_string("SMTP_PASSWORD")?,
|
||||
smtp_relayer: config.get_string("SMTP_RELAYER")?,
|
||||
rabbitmq_address: config.get_string("RABBITMQ_ADDRESS")?,
|
||||
rabbitmq_exchange: config.get_string("RABBITMQ_EXCHANGE")?,
|
||||
rabbitmq_queue: config.get_string("RABBITMQ_QUEUE")?,
|
||||
rabbitmq_routing_key: config.get_string("RABBITMQ_ROUTING_KEY")?,
|
||||
surrealdb_address: config.get_string("SURREALDB_ADDRESS")?,
|
||||
surrealdb_username: config.get_string("SURREALDB_USERNAME")?,
|
||||
surrealdb_password: config.get_string("SURREALDB_PASSWORD")?,
|
||||
surrealdb_namespace: config.get_string("SURREALDB_NAMESPACE")?,
|
||||
surrealdb_database: config.get_string("SURREALDB_DATABASE")?,
|
||||
})
|
||||
}
|
||||
@@ -1,23 +1,81 @@
|
||||
use std::env;
|
||||
|
||||
use lettre::address::AddressError;
|
||||
use lettre::message::MultiPart;
|
||||
use lettre::{transport::smtp::authentication::Credentials, SmtpTransport};
|
||||
use lettre::{Message, Transport};
|
||||
use minijinja::context;
|
||||
use minijinja_autoreload::AutoReloader;
|
||||
use thiserror::Error;
|
||||
use tracing::info;
|
||||
|
||||
pub struct Mailer {
|
||||
pub mailer: SmtpTransport,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EmailError {
|
||||
#[error("Email construction error: {0}")]
|
||||
EmailParsingError(#[from] AddressError),
|
||||
|
||||
#[error("Email sending error: {0}")]
|
||||
SendingError(#[from] lettre::transport::smtp::Error),
|
||||
|
||||
#[error("Body constructing error: {0}")]
|
||||
BodyError(#[from] lettre::error::Error),
|
||||
|
||||
#[error("Templating error: {0}")]
|
||||
TemplatingError(#[from] minijinja::Error),
|
||||
}
|
||||
|
||||
impl Mailer {
|
||||
pub fn new() -> Self {
|
||||
let creds = Credentials::new(
|
||||
env::var("SMTP_USERNAME").unwrap().to_owned(),
|
||||
env::var("SMTP_PASSWORD").unwrap().to_owned(),
|
||||
);
|
||||
pub fn new(
|
||||
username: String,
|
||||
relayer: String,
|
||||
password: String,
|
||||
) -> Result<Self, lettre::transport::smtp::Error> {
|
||||
let creds = Credentials::new(username, password);
|
||||
|
||||
let mailer = SmtpTransport::relay(env::var("SMTP_RELAYER").unwrap().as_str())
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
let mailer = SmtpTransport::relay(&relayer)?.credentials(creds).build();
|
||||
|
||||
Mailer { mailer }
|
||||
Ok(Mailer { mailer })
|
||||
}
|
||||
|
||||
pub fn send_email_verification(
|
||||
&self,
|
||||
email_to: &str,
|
||||
verification_code: &str,
|
||||
templates: &AutoReloader,
|
||||
) -> Result<(), EmailError> {
|
||||
let name = email_to
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap_or("User")
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
|
||||
.collect::<String>();
|
||||
|
||||
let context = context! {
|
||||
name => name,
|
||||
verification_code => verification_code
|
||||
};
|
||||
|
||||
let env = templates.acquire_env()?;
|
||||
let html = env
|
||||
.get_template("email/email_verification.html")?
|
||||
.render(&context)?;
|
||||
let plain = env
|
||||
.get_template("email/email_verification.txt")?
|
||||
.render(&context)?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from("Admin <minne@starks.cloud>".parse()?)
|
||||
.reply_to("Admin <minne@starks.cloud>".parse()?)
|
||||
.to(format!("{} <{}>", name, email_to).parse()?)
|
||||
.subject("Verify Your Email Address")
|
||||
.multipart(MultiPart::alternative_plain_html(plain, html))?;
|
||||
|
||||
info!("Sending email to: {}", email_to);
|
||||
self.mailer.send(&email)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod embedding;
|
||||
pub mod mailer;
|
||||
|
||||
77
templates/email/email_verification.html
Normal file
77
templates/email/email_verification.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{# email_verification.html #}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Verification</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
margin: 20px auto;
|
||||
max-width: 400px;
|
||||
}
|
||||
.verification-code {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
letter-spacing: 2px;
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Email Verification</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p>Hello {{ name }},</p>
|
||||
<p>Please use the following verification code to complete your registration:</p>
|
||||
|
||||
<div class="verification-code">
|
||||
{{ verification_code }}
|
||||
</div>
|
||||
|
||||
<p>This code will expire in 30 minutes.</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>Security Notice:</strong> If you didn't request this verification code, please ignore this email. Someone might have entered your email address by mistake.</p>
|
||||
<p>For your security, never share this code with anyone, including those claiming to be from our support team.</p>
|
||||
<p>This is an automated message, please do not reply to this email.</p>
|
||||
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
||||
<p>© 2024 Your Company Name. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
templates/email/email_verification.txt
Normal file
20
templates/email/email_verification.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
{# email_verification.txt #}
|
||||
Hello {{ name }},
|
||||
|
||||
Thank you for registering. To complete your registration, please use the following verification code:
|
||||
|
||||
{{ verification_code }}
|
||||
|
||||
This code will expire in 30 minutes.
|
||||
|
||||
IMPORTANT SECURITY INFORMATION:
|
||||
- If you didn't request this verification code, please ignore this email.
|
||||
- Never share this code with anyone, including those claiming to be from our support team.
|
||||
- This is an automated message, please do not reply to this email.
|
||||
|
||||
For your security, if you did not initiate this request, you can safely ignore this email. Someone might have entered your email address by mistake.
|
||||
|
||||
Best regards,
|
||||
Your Company Name
|
||||
|
||||
© 2024 Your Company Name. All rights reserved.
|
||||
@@ -5,8 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}minnet{% endblock %}</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
||||
<script src="assets/htmx.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<link rel="manifest" href="/assets/manifest.json">
|
||||
<!-- Optional but recommended for iOS support -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user