Implement contact form backend

This commit is contained in:
Garrett Mills 2022-04-05 14:24:36 -05:00
parent e643bf6df0
commit e3c3b93818
12 changed files with 399 additions and 11 deletions

2
ex
View File

@ -125,6 +125,8 @@ if [ ! -d "./node_modules" ]; then
echoRun "$ENV_PNPM" i
echoRun ./node_modules/.bin/ts-patch i
if [ ! -f "./.env" ]; then
echoRun cp example.env .env
fi

View File

@ -8,9 +8,10 @@
"lib": "lib"
},
"dependencies": {
"@extollo/lib": "^0.9.31",
"@extollo/lib": "^0.9.32",
"copyfiles": "^2.4.1",
"feed": "^4.2.2",
"gotify": "^1.1.0",
"rimraf": "^3.0.2",
"ts-expose-internals": "^4.5.4",
"ts-patch": "^2.0.1",

View File

@ -2,9 +2,10 @@ lockfileVersion: 5.3
specifiers:
'@extollo/cc': ^0.6.0
'@extollo/lib': ^0.9.31
'@extollo/lib': ^0.9.32
copyfiles: ^2.4.1
feed: ^4.2.2
gotify: ^1.1.0
rimraf: ^3.0.2
ts-expose-internals: ^4.5.4
ts-patch: ^2.0.1
@ -13,9 +14,10 @@ specifiers:
zod: ^3.11.6
dependencies:
'@extollo/lib': 0.9.31
'@extollo/lib': 0.9.32
copyfiles: 2.4.1
feed: 4.2.2
gotify: 1.1.0
rimraf: 3.0.2
ts-expose-internals: 4.5.4
ts-patch: 2.0.1_typescript@4.3.2
@ -112,8 +114,8 @@ packages:
- supports-color
dev: true
/@extollo/lib/0.9.31:
resolution: {integrity: sha512-sYtqWTL+hmkPZ44fwWdRsHK1081m98547r9snFm8i+ou13Npw3i4RBf8k+uFRUCayMM4PKVWqgo0bZJLvO4W1g==}
/@extollo/lib/0.9.32:
resolution: {integrity: sha512-3DuRrLFmYY6w0rK5QKSlNWbGZnel6Lj2vjSGwwbj1IEJXGcZSA+TMXLghAwPkKyUsyX0WwdXVn6S51jmRHyWJw==}
dependencies:
'@atao60/fse-cli': 0.1.7
'@extollo/ui': 0.1.0_@types+node@14.18.12
@ -305,6 +307,18 @@ packages:
resolution: {integrity: sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==}
dev: false
/@sindresorhus/is/2.1.1:
resolution: {integrity: sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==}
engines: {node: '>=10'}
dev: false
/@szmarczak/http-timer/4.0.6:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
dependencies:
defer-to-connect: 2.0.1
dev: false
/@tsconfig/node10/1.0.8:
resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
@ -333,6 +347,15 @@ packages:
'@types/node': 17.0.23
dev: false
/@types/cacheable-request/6.0.2:
resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==}
dependencies:
'@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.4
'@types/node': 17.0.23
'@types/responselike': 1.0.0
dev: false
/@types/cli-color/2.0.2:
resolution: {integrity: sha512-1ErQIcmNHtNViGKTtB/TIKqMkC2RkKI2nBneCr9hSCPo9H05g9VzjlaXPW3H0vaI8zFGjJZvSav+VKDKCtKgKA==}
dev: true
@ -353,18 +376,32 @@ packages:
'@types/minimatch': 3.0.5
'@types/node': 17.0.23
/@types/http-cache-semantics/4.0.1:
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
dev: false
/@types/ioredis/4.28.10:
resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==}
dependencies:
'@types/node': 17.0.23
dev: false
/@types/json-buffer/3.0.0:
resolution: {integrity: sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==}
dev: false
/@types/jsonwebtoken/8.5.8:
resolution: {integrity: sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==}
dependencies:
'@types/node': 17.0.23
dev: false
/@types/keyv/3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
'@types/node': 17.0.23
dev: false
/@types/mime-types/2.1.1:
resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==}
dev: false
@ -404,6 +441,12 @@ packages:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: false
/@types/responselike/1.0.0:
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
dependencies:
'@types/node': 17.0.23
dev: false
/@types/rimraf/3.0.2:
resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==}
dependencies:
@ -638,6 +681,27 @@ packages:
dicer: 0.3.0
dev: false
/cacheable-lookup/2.0.1:
resolution: {integrity: sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg==}
engines: {node: '>=10'}
dependencies:
'@types/keyv': 3.1.4
keyv: 4.2.1
dev: false
/cacheable-request/7.0.2:
resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
engines: {node: '>=8'}
dependencies:
clone-response: 1.0.2
get-stream: 5.2.0
http-cache-semantics: 4.1.0
keyv: 4.2.1
lowercase-keys: 2.0.0
normalize-url: 6.1.0
responselike: 2.0.0
dev: false
/call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
@ -734,6 +798,12 @@ packages:
wrap-ansi: 7.0.0
dev: false
/clone-response/1.0.2:
resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=}
dependencies:
mimic-response: 1.0.1
dev: false
/clone/1.0.4:
resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=}
engines: {node: '>=0.8'}
@ -771,6 +841,14 @@ packages:
resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==}
dev: false
/compress-brotli/1.3.6:
resolution: {integrity: sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==}
engines: {node: '>= 12'}
dependencies:
'@types/json-buffer': 3.0.0
json-buffer: 3.0.1
dev: false
/concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
@ -843,11 +921,23 @@ packages:
dependencies:
ms: 2.1.2
/decompress-response/5.0.0:
resolution: {integrity: sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==}
engines: {node: '>=10'}
dependencies:
mimic-response: 2.1.0
dev: false
/defaults/1.0.3:
resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=}
dependencies:
clone: 1.0.4
/defer-to-connect/2.0.1:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
engines: {node: '>=10'}
dev: false
/delegates/1.0.0:
resolution: {integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=}
dev: false
@ -892,6 +982,10 @@ packages:
engines: {node: '>=10'}
dev: false
/duplexer3/0.1.4:
resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=}
dev: false
/ecdsa-sig-formatter/1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
@ -901,6 +995,12 @@ packages:
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
/end-of-stream/1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
once: 1.4.0
dev: false
/es5-ext/0.10.59:
resolution: {integrity: sha512-cOgyhW0tIJyQY1Kfw6Kr0viu9ZlUctVchRMZ7R0HiH3dxTSp5zJDLecwxUqPUrGKMsgBI1wd1FL+d9Jxfi4cLw==}
engines: {node: '>=0.10'}
@ -1096,6 +1196,13 @@ packages:
has-symbols: 1.0.3
dev: false
/get-stream/5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
dependencies:
pump: 3.0.0
dev: false
/glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -1131,6 +1238,33 @@ packages:
merge2: 1.4.1
slash: 3.0.0
/got/10.7.0:
resolution: {integrity: sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg==}
engines: {node: '>=10'}
dependencies:
'@sindresorhus/is': 2.1.1
'@szmarczak/http-timer': 4.0.6
'@types/cacheable-request': 6.0.2
cacheable-lookup: 2.0.1
cacheable-request: 7.0.2
decompress-response: 5.0.0
duplexer3: 0.1.4
get-stream: 5.2.0
lowercase-keys: 2.0.0
mimic-response: 2.1.0
p-cancelable: 2.1.1
p-event: 4.2.0
responselike: 2.0.0
to-readable-stream: 2.1.0
type-fest: 0.10.0
dev: false
/gotify/1.1.0:
resolution: {integrity: sha512-f3PUh08i+1JCJuzv9CRYuIx1QOb9DoWXRvWaPttgiZEG7XtetDQVv6S9cr6suPNKkZI4ry8dqdU2qdb9AiPinw==}
dependencies:
got: 10.7.0
dev: false
/graceful-fs/4.2.9:
resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==}
@ -1173,6 +1307,10 @@ packages:
dependencies:
function-bind: 1.1.1
/http-cache-semantics/4.1.0:
resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==}
dev: false
/https-proxy-agent/5.0.0:
resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==}
engines: {node: '>= 6'}
@ -1339,6 +1477,10 @@ packages:
resolution: {integrity: sha1-Fzb939lyTyijaCrcYjCufk6Weds=}
dev: false
/json-buffer/3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
dev: false
/jsonc-parser/3.0.0:
resolution: {integrity: sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==}
dev: false
@ -1393,6 +1535,13 @@ packages:
safe-buffer: 5.1.2
dev: false
/keyv/4.2.1:
resolution: {integrity: sha512-cAJq5cTfxQdq1DHZEVNpnk4mEvhP+8UP8UQftLtTtJ98beKkRHf+62M0mIDM2u/IWXyP8bmGB375/6uGdSX2MA==}
dependencies:
compress-brotli: 1.3.6
json-buffer: 3.0.1
dev: false
/kind-of/6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
@ -1447,6 +1596,11 @@ packages:
chalk: 4.1.2
is-unicode-supported: 0.1.0
/lowercase-keys/2.0.0:
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
engines: {node: '>=8'}
dev: false
/lru-cache/6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@ -1523,6 +1677,16 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
/mimic-response/1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
dev: false
/mimic-response/2.1.0:
resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
engines: {node: '>=8'}
dev: false
/minimatch/3.0.4:
resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
dependencies:
@ -1624,6 +1788,11 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
/normalize-url/6.1.0:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
dev: false
/npmlog/5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
dependencies:
@ -1670,11 +1839,35 @@ packages:
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=}
engines: {node: '>=0.10.0'}
/p-cancelable/2.1.1:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
engines: {node: '>=8'}
dev: false
/p-event/4.2.0:
resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==}
engines: {node: '>=8'}
dependencies:
p-timeout: 3.2.0
dev: false
/p-finally/1.0.0:
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
engines: {node: '>=4'}
dev: false
/p-map/2.1.0:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'}
dev: false
/p-timeout/3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
dependencies:
p-finally: 1.0.0
dev: false
/packet-reader/1.0.0:
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
dev: false
@ -1888,6 +2081,13 @@ packages:
pug-strip-comments: 2.0.0
dev: false
/pump/3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
dev: false
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -1969,6 +2169,12 @@ packages:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
/responselike/2.0.0:
resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==}
dependencies:
lowercase-keys: 2.0.0
dev: false
/restore-cursor/3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
@ -2221,6 +2427,11 @@ packages:
engines: {node: '>=4'}
dev: false
/to-readable-stream/2.1.0:
resolution: {integrity: sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==}
engines: {node: '>=8'}
dev: false
/to-regex-range/5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -2393,6 +2604,11 @@ packages:
resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=}
dev: false
/type-fest/0.10.0:
resolution: {integrity: sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==}
engines: {node: '>=8'}
dev: false
/type-fest/0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}

View File

@ -1,9 +1,19 @@
import {ORMUser, Singleton, Unit} from '@extollo/lib'
import {Config, Inject, ORMUser, Singleton, Unit} from '@extollo/lib'
import {User} from './models/User.model'
import {Gotify} from 'gotify'
@Singleton()
export class AppUnit extends Unit {
@Inject()
protected readonly config!: Config
async up(): Promise<void> {
this.container().registerStaticOverride(ORMUser, User)
const gotify = new Gotify({
server: this.config.safe('gotify.server').string(),
})
this.container().registerSingletonInstance(Gotify, gotify)
}
}

View File

@ -0,0 +1,6 @@
import { env } from '@extollo/lib'
export default {
server: env('GOTIFY_SERVER'),
app: env('GOTIFY_TOKEN'),
}

View File

@ -1,6 +1,22 @@
import {Controller, view, Injectable, SecurityContext, Inject, Collection, Config, Routing, file, Application, plaintext} from '@extollo/lib'
import {
Controller,
view,
Injectable,
SecurityContext,
Inject,
Collection,
Config,
Routing,
file,
Application,
make,
Valid,
} from '@extollo/lib'
import {WorkItem} from '../../models/WorkItem.model'
import {FeedPost} from '../../models/FeedPost.model'
import {ContactForm} from '../../types/ContactForm.type'
import {ContactSubmission} from '../../models/ContactSubmission.model'
import {Gotify} from 'gotify'
@Injectable()
export class Home extends Controller {
@ -13,6 +29,9 @@ export class Home extends Controller {
@Inject()
protected readonly routing!: Routing
@Inject()
protected readonly gotify!: Gotify
public async welcome(feedPosts: Collection<FeedPost>) {
const workItems = await this.getWorkItems()
@ -88,4 +107,29 @@ export class Home extends Controller {
.appPath('resources', 'assets', 'humans.txt')
.read()
}
async contact(data: Valid<ContactForm>) {
const submission = make<ContactSubmission>(ContactSubmission)
submission.name = data.name
submission.email = data.email
submission.message = data.message
await submission.save()
this.gotify.send({
app: this.config.get('gotify.app'),
title: `Contact form submission from ${data.name}`,
message: [
`From: ${data.name}`,
`E-mail: ${data.email}`,
'Message:',
data.message,
].join('\n'),
})
return view('message', {
title: 'Message Sent',
message: 'Your message has been sent. Thanks! I\'ll be in touch soon.',
buttonAction: this.routing.getNamedPath('home').toRemote,
})
}
}

View File

@ -0,0 +1,15 @@
import {ParameterMiddleware, Injectable, Either, ResponseObject, Validator, Valid, right} from '@extollo/lib'
import {ContactForm} from '../../../types/ContactForm.type'
/**
* ContactForm Middleware
* --------------------------------------------
* Parse the contact form data and validate it. Provide the fields as middleware.
*/
@Injectable()
export class ValidContactForm extends ParameterMiddleware<Valid<ContactForm>> {
async handle(): Promise<Either<ResponseObject, Valid<ContactForm>>> {
const validator = new Validator<ContactForm>()
return right(validator.parse(this.request.input()))
}
}

View File

@ -6,6 +6,7 @@ import {GoLinks} from '../controllers/GoLinks.controller'
import {Feed} from '../controllers/Feed.controller'
import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware'
import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware'
import {ValidContactForm} from '../middlewares/parameters/ValidContactForm.middleware'
Route
.group('/', () => {
@ -14,6 +15,11 @@ Route
.calls<Home>(Home, home => home.welcome)
.alias('home')
Route.post('/contact')
.parameterMiddleware(ValidContactForm)
.calls<Home>(Home, home => home.contact)
.alias('contact')
Route.get('/humans.txt')
.calls<Home>(Home, home => home.humans)

View File

@ -0,0 +1,51 @@
import {Injectable, Migration, Inject, DatabaseService, FieldType, raw} from '@extollo/lib'
/**
* CreateContactSubmissionsTableMigration
* ----------------------------------
* Put some description here.
*/
@Injectable()
export default class CreateContactSubmissionsTableMigration extends Migration {
@Inject()
protected readonly db!: DatabaseService
/**
* Apply the migration.
*/
async up(): Promise<void> {
const schema = this.db.get().schema()
const table = await schema.table('contact_submissions')
table.primaryKey('contact_submission_id').required()
table.column('email')
.type(FieldType.varchar)
.required()
table.column('name')
.type(FieldType.varchar)
.required()
table.column('message')
.type(FieldType.text)
table.column('sent_at')
.type(FieldType.timestamp)
.default(raw('NOW()'))
await schema.commit(table)
}
/**
* Undo the migration.
*/
async down(): Promise<void> {
const schema = this.db.get().schema()
const table = await schema.table('contact_submissions')
table.dropIfExists()
await schema.commit(table)
}
}

View File

@ -0,0 +1,27 @@
import {Field, FieldType, Injectable, Model} from '@extollo/lib'
/**
* ContactSubmission Model
* -----------------------------------
* A message submitted via the contact form on my website.
*/
@Injectable()
export class ContactSubmission extends Model<ContactSubmission> {
protected static table = 'contact_submissions'
protected static key = 'contact_submission_id'
@Field(FieldType.serial, 'contact_submission_id')
protected id?: number
@Field(FieldType.varchar)
public email!: string
@Field(FieldType.varchar)
public name!: string
@Field(FieldType.text)
public message!: string
@Field(FieldType.timestamp, 'sent_at')
public sentAt = new Date()
}

View File

@ -40,13 +40,11 @@ block content
p I'd love to hear from you if you have questions or inquiries related to me or my projects. You can get in touch by text, e-mail, or using this form. I also occasionally share thoughts on my <a href="/blog">blog</a>.
p <b>E-mail:</b> <a href="mailto:shout@garrettmills.dev">shout@garrettmills.dev</a>
.form
form#contact-form
form#contact-form(method='post' action=named('contact'))
.form-group
input#contactEmail.form-control(type='email' name='email' placeholder='E-Mail Address' required)
.form-group
input#contactFirst.form-control(name='first' placeholder='First Name' required)
.form-group
input#contactLast.form-control(name='last' placeholder='Last Name' required)
input#contactFirst.form-control(name='name' placeholder='Name' required)
.form-group
textarea.form-control#contactMessage(name='message' placeholder='Message' required rows=6)
.form-group

View File

@ -0,0 +1,12 @@
/** A contact form submission. */
export interface ContactForm {
/** @email */
email: string
/** The submitter's name */
name: string
/** The body of the contact form submission. */
message: string
}