mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
718 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b96b4151 | ||
|
|
e9064643a6 | ||
|
|
667592411f | ||
|
|
dfdf4981cb | ||
|
|
0f377bdec6 | ||
|
|
ba17474e62 | ||
|
|
c09ad99739 | ||
|
|
799efe1772 | ||
|
|
1d79400df5 | ||
|
|
cc4a2e087f | ||
|
|
64a76f3b9f | ||
|
|
7c1aaab291 | ||
|
|
63556d163a | ||
|
|
c49c0e4ad5 | ||
|
|
3f2121f272 | ||
|
|
ebdcf778be | ||
|
|
fb669df9cf | ||
|
|
cedf7d0733 | ||
|
|
00db83f478 | ||
|
|
03b7f92a44 | ||
|
|
d542a4273d | ||
|
|
dcb27ca543 | ||
|
|
78635b8ba1 | ||
|
|
e18d31ee9b | ||
|
|
0db5fb64a8 | ||
|
|
e36d28eb99 | ||
|
|
dd331f75c9 | ||
|
|
aa11697ee2 | ||
|
|
fdd698dd0a | ||
|
|
c8df588401 | ||
|
|
a8373338c2 | ||
|
|
15abd1f51b | ||
|
|
71407cc86d | ||
|
|
85a3fed127 | ||
|
|
6b8f0d6cdf | ||
|
|
43441831d4 | ||
|
|
319cff8fe1 | ||
|
|
5904204465 | ||
|
|
6c8cc92a67 | ||
|
|
693860acef | ||
|
|
f7626ec15b | ||
|
|
03c6bbc81f | ||
|
|
bbe7ef1b2b | ||
|
|
027b95da15 | ||
|
|
e9c33a808f | ||
|
|
2545e1204f | ||
|
|
970d334b59 | ||
|
|
50a18dc461 | ||
|
|
0dcc77eb0d | ||
|
|
cd84592be1 | ||
|
|
df6de32a4a | ||
|
|
3d24772caa | ||
|
|
1a106e59fc | ||
|
|
290460c095 | ||
|
|
17802dc216 | ||
|
|
0de52a396a | ||
|
|
64705e582d | ||
|
|
b09cdcec1e | ||
|
|
87bb34f3ba | ||
|
|
5b53208a3e | ||
|
|
7a687bba43 | ||
|
|
aafac49bcb | ||
|
|
201af7b88a | ||
|
|
43ddd394c2 | ||
|
|
f248f9cd26 | ||
|
|
24f6168a7e | ||
|
|
46b0d7b24b | ||
|
|
37bc557278 | ||
|
|
29d7b6dfdb | ||
|
|
ec84792034 | ||
|
|
250ab24654 | ||
|
|
9b8770bb20 | ||
|
|
192d35d2dd | ||
|
|
9d2f7a15dd | ||
|
|
95ca7ab014 | ||
|
|
d29a33a95e | ||
|
|
a2486b67d7 | ||
|
|
abcd887e49 | ||
|
|
f400387630 | ||
|
|
3613a0cae1 | ||
|
|
13e16d1a51 | ||
|
|
a2eb12d6b9 | ||
|
|
dbb0a33179 | ||
|
|
32bb4502d6 | ||
|
|
f12642873d | ||
|
|
e9b9d6c363 | ||
|
|
a92f074099 | ||
|
|
5fbaed57cc | ||
|
|
9e24442d84 | ||
|
|
7737a586ab | ||
|
|
ce97c26b05 | ||
|
|
fa52643d48 | ||
|
|
be3ea8e198 | ||
|
|
574dcb3048 | ||
|
|
bb20f16b0f | ||
|
|
8284241be6 | ||
|
|
a0fb68a68c | ||
|
|
2e1497795d | ||
|
|
ddeab13cc3 | ||
|
|
8841a51376 | ||
|
|
e8e380bd22 | ||
|
|
ce5ad91ef5 | ||
|
|
b253772152 | ||
|
|
b0cb9e9b8d | ||
|
|
085706eae5 | ||
|
|
f57dcfa894 | ||
|
|
1b9ff1754a | ||
|
|
86a49de886 | ||
|
|
f6473b263b | ||
|
|
eae0aaf2a5 | ||
|
|
e086ce3684 | ||
|
|
3401dcf402 | ||
|
|
30ec77ef56 | ||
|
|
5e6e05cfd3 | ||
|
|
d06187879b | ||
|
|
d5e6b7a5c7 | ||
|
|
609006d199 | ||
|
|
1eb6f46f3c | ||
|
|
ad03613316 | ||
|
|
d71194b3b8 | ||
|
|
7b43acec09 | ||
|
|
8e24686578 | ||
|
|
2d91fdfaed | ||
|
|
ce93c01039 | ||
|
|
0631daf61b | ||
|
|
50c3c64db6 | ||
|
|
5b22a41aa6 | ||
|
|
e2be93ec85 | ||
|
|
bc6c7a10dc | ||
|
|
bca0d3ee6b | ||
|
|
c860899f8e | ||
|
|
fba8c171b6 | ||
|
|
557deee352 | ||
|
|
9ec1d83de9 | ||
|
|
6760a9c89a | ||
|
|
86ccccb95d | ||
|
|
8cca485930 | ||
|
|
353631bcda | ||
|
|
2f79eabd1b | ||
|
|
0ef1a9b118 | ||
|
|
7a374d9730 | ||
|
|
faa578b5b5 | ||
|
|
d59757c8fb | ||
|
|
88e5142049 | ||
|
|
f6c9000bec | ||
|
|
d3b3281ca4 | ||
|
|
55178006c8 | ||
|
|
92b65bad20 | ||
|
|
443721dc75 | ||
|
|
997335205f | ||
|
|
2d4c865709 | ||
|
|
5f4290add1 | ||
|
|
f553f4e596 | ||
|
|
acdeb8bc28 | ||
|
|
030b90eb76 | ||
|
|
022a32358d | ||
|
|
75f71dcc43 | ||
|
|
dae7e1d065 | ||
|
|
d0b680c20c | ||
|
|
95bc774d2d | ||
|
|
11c680471f | ||
|
|
c5ccde298f | ||
|
|
c969cd738f | ||
|
|
be62fc9127 | ||
|
|
da5754d720 | ||
|
|
658d03bc02 | ||
|
|
862f013bda | ||
|
|
c0d250a35c | ||
|
|
26580f72ca | ||
|
|
3ff418945c | ||
|
|
c729a06e25 | ||
|
|
b62a13ce76 | ||
|
|
9dca684e4c | ||
|
|
5d7433674c | ||
|
|
458a336526 | ||
|
|
9e16e30ad0 | ||
|
|
de0cce7b64 | ||
|
|
7b315dc87a | ||
|
|
dcf04f040d | ||
|
|
6d795c6370 | ||
|
|
827f677af8 | ||
|
|
be96d45275 | ||
|
|
7ef6e3ad8f | ||
|
|
df83bdd329 | ||
|
|
9d1341e02a | ||
|
|
1f249af022 | ||
|
|
5f9f607290 | ||
|
|
16baa7ad74 | ||
|
|
c4197e6cd8 | ||
|
|
8aa2ec925a | ||
|
|
b3ac7064ef | ||
|
|
91f554187e | ||
|
|
e161478614 | ||
|
|
92e2de76ba | ||
|
|
5644f89db0 | ||
|
|
8ea8382c7d | ||
|
|
869c3c86be | ||
|
|
cd865d70c6 | ||
|
|
5bb2e3d5ce | ||
|
|
e80c351ee6 | ||
|
|
359fa1752f | ||
|
|
259de77a12 | ||
|
|
f15ef80839 | ||
|
|
62d2249f40 | ||
|
|
14845e77e0 | ||
|
|
fa664ea918 | ||
|
|
bbb168d764 | ||
|
|
d5ba3a63e4 | ||
|
|
afc605afd0 | ||
|
|
cdbe6c6e8c | ||
|
|
b184273456 | ||
|
|
fb7de645e5 | ||
|
|
35f35b8bbe | ||
|
|
4c5e312f11 | ||
|
|
708f6d7b9b | ||
|
|
5b72e181e6 | ||
|
|
4441c543cc | ||
|
|
933f8650ce | ||
|
|
26c910bf26 | ||
|
|
037919a548 | ||
|
|
771527c891 | ||
|
|
13c8709c19 | ||
|
|
043538054e | ||
|
|
d366dc3b0c | ||
|
|
35f1332138 | ||
|
|
ac040d90a8 | ||
|
|
0e50276308 | ||
|
|
1c0041326b | ||
|
|
8d13b1b0e3 | ||
|
|
a267d94b3e | ||
|
|
4ccbba5b4b | ||
|
|
daf25fcc12 | ||
|
|
fc8d365c49 | ||
|
|
e7e66a6285 | ||
|
|
ad26450d8b | ||
|
|
fcf0fb8605 | ||
|
|
da2efa7e8a | ||
|
|
4018d38148 | ||
|
|
6affa67561 | ||
|
|
0a543cec42 | ||
|
|
179faac0a0 | ||
|
|
4cfacc5012 | ||
|
|
a407a23101 | ||
|
|
df33d4abd4 | ||
|
|
28a5939f62 | ||
|
|
467b25104e | ||
|
|
8ee3cd0396 | ||
|
|
d471a61fbf | ||
|
|
df0413038e | ||
|
|
9180d600a6 | ||
|
|
8bea3d9336 | ||
|
|
1f88d5678b | ||
|
|
061e72210f | ||
|
|
ef8fa091b9 | ||
|
|
21e8171355 | ||
|
|
5509be5281 | ||
|
|
6c7645ea3d | ||
|
|
4bd3b5cb29 | ||
|
|
2e08de3546 | ||
|
|
956338f61f | ||
|
|
9f87861f88 | ||
|
|
db6357a845 | ||
|
|
adc82278b2 | ||
|
|
22945f6066 | ||
|
|
9646552e46 | ||
|
|
2ffc6c1f52 | ||
|
|
9338384649 | ||
|
|
b6cfc03b01 | ||
|
|
997bd8ce44 | ||
|
|
e8b0544735 | ||
|
|
f1ff5c1d54 | ||
|
|
90471c362f | ||
|
|
201f521d60 | ||
|
|
cb25be8962 | ||
|
|
a1291b1951 | ||
|
|
eed66e3c48 | ||
|
|
9544aab2ce | ||
|
|
809507d9c3 | ||
|
|
b3e88f9d99 | ||
|
|
eda6ad0e44 | ||
|
|
86eacb0ad8 | ||
|
|
06830baf3d | ||
|
|
e82faeba40 | ||
|
|
f2cf7b123d | ||
|
|
e499fa9ace | ||
|
|
d6e73251d8 | ||
|
|
7ee2984459 | ||
|
|
5b89f163de | ||
|
|
291b441d3f | ||
|
|
89751633f1 | ||
|
|
50464886f3 | ||
|
|
9d0fc082c0 | ||
|
|
c85bf5cebd | ||
|
|
4f49f343c9 | ||
|
|
226b5bfaff | ||
|
|
73013306d6 | ||
|
|
0b7c641e32 | ||
|
|
b7ab70e3de | ||
|
|
f09c27eefa | ||
|
|
0f4482487b | ||
|
|
edc537316f | ||
|
|
4667ff64e1 | ||
|
|
d3d6dc56b8 | ||
|
|
b5525f137a | ||
|
|
5c619d9553 | ||
|
|
693d419bd9 | ||
|
|
9bccaba360 | ||
|
|
c4e82205b6 | ||
|
|
ef9c9e690f | ||
|
|
a4cc75a3aa | ||
|
|
633cef3450 | ||
|
|
c7af544e25 | ||
|
|
43ae4625dd | ||
|
|
a53deeeebf | ||
|
|
f84f063155 | ||
|
|
7f411db4dd | ||
|
|
f5ae307fba | ||
|
|
f0989aa2d7 | ||
|
|
d8ea3fe73c | ||
|
|
a1c1e86059 | ||
|
|
c86a1f84db | ||
|
|
92b5df1cc8 | ||
|
|
3e7826607f | ||
|
|
06f09cdbf1 | ||
|
|
81ec09a2ed | ||
|
|
7977cd7394 | ||
|
|
71ad07fad0 | ||
|
|
77de70ba82 | ||
|
|
b97b2163d5 | ||
|
|
b2aed24d8a | ||
|
|
6e6f24417a | ||
|
|
725efcfa91 | ||
|
|
71c326bc55 | ||
|
|
4b1feca11d | ||
|
|
02f9571b8b | ||
|
|
9e3b08c50d | ||
|
|
84ac36b1e2 | ||
|
|
690547bbf2 | ||
|
|
8d00ee496f | ||
|
|
aea8cbf405 | ||
|
|
0d3a2032a2 | ||
|
|
d619976b10 | ||
|
|
72034391fb | ||
|
|
5cda059a91 | ||
|
|
3109135a17 | ||
|
|
eca61933bf | ||
|
|
df72068e5c | ||
|
|
f093958833 | ||
|
|
d98ac5e61d | ||
|
|
7a730ac944 | ||
|
|
a99e370b1c | ||
|
|
94ad6e9ea0 | ||
|
|
eaec5447f5 | ||
|
|
c3aee4df8f | ||
|
|
27b0820906 | ||
|
|
1330e78169 | ||
|
|
739c54d821 | ||
|
|
0e8e5bf2ad | ||
|
|
f235e72f01 | ||
|
|
9b250bf83f | ||
|
|
138ffcb7a6 | ||
|
|
7da152a412 | ||
|
|
ec4e2d2c80 | ||
|
|
984d8512e9 | ||
|
|
e2e0b81564 | ||
|
|
770ee60402 | ||
|
|
2aa7d1ce60 | ||
|
|
578dd9da87 | ||
|
|
b129a75255 | ||
|
|
7ec2594d7f | ||
|
|
e1f729f2ed | ||
|
|
e53d1931c5 | ||
|
|
4373e6fa62 | ||
|
|
5ecdf926b6 | ||
|
|
e47b109f9d | ||
|
|
be244b2c68 | ||
|
|
155a3ccd0b | ||
|
|
5a16495864 | ||
|
|
b0c74d3ce2 | ||
|
|
46360e2f4a | ||
|
|
f2378f8e7f | ||
|
|
ca6aa40850 | ||
|
|
b015e4a9d6 | ||
|
|
8dc4bcb06f | ||
|
|
466c181ad1 | ||
|
|
cd4dc918cb | ||
|
|
18cea13ddd | ||
|
|
ab5795101f | ||
|
|
600d7bcbda | ||
|
|
25b908e311 | ||
|
|
0269d584aa | ||
|
|
072e63e98f | ||
|
|
95949508ba | ||
|
|
1564df342a | ||
|
|
1b2a6029bb | ||
|
|
c131372e37 | ||
|
|
b830d90ba4 | ||
|
|
e6feafcb87 | ||
|
|
52e55e44f2 | ||
|
|
1945b15e2e | ||
|
|
4f8ab5da28 | ||
|
|
188b1cba94 | ||
|
|
5da1021088 | ||
|
|
21ae755018 | ||
|
|
ca3e35e066 | ||
|
|
7d6a5fa947 | ||
|
|
a9e41fa6b4 | ||
|
|
f6f33c2482 | ||
|
|
9ef078bd57 | ||
|
|
329281cd53 | ||
|
|
1db9fa5a37 | ||
|
|
8ba039ff25 | ||
|
|
0df0b2c3ff | ||
|
|
e058990e31 | ||
|
|
f7865e5d9c | ||
|
|
7aeb34ec5f | ||
|
|
1f9400e811 | ||
|
|
a3e46f28a3 | ||
|
|
01f026a3d3 | ||
|
|
8824db222b | ||
|
|
dce170cee1 | ||
|
|
ef7a11e27a | ||
|
|
75b22cfddf | ||
|
|
65342a2a8d | ||
|
|
18978b94be | ||
|
|
c989573565 | ||
|
|
67ce7da21a | ||
|
|
fb2972695a | ||
|
|
2f47f81af8 | ||
|
|
6d6ee8bf3f | ||
|
|
881eb58a35 | ||
|
|
80bc7cd223 | ||
|
|
87f494fea8 | ||
|
|
a24e533e4c | ||
|
|
ebb8b27f85 | ||
|
|
41c210e75a | ||
|
|
2a50a455d8 | ||
|
|
6896c4cd1d | ||
|
|
9560572a40 | ||
|
|
4dffb9c3c0 | ||
|
|
b8cb3c4d78 | ||
|
|
6f54b05d5a | ||
|
|
d051d69aea | ||
|
|
02f0c43cbd | ||
|
|
14cce42091 | ||
|
|
a1c26cec04 | ||
|
|
42fc1c018c | ||
|
|
f3e740bab3 | ||
|
|
bbdf47d6f4 | ||
|
|
5faf93d6be | ||
|
|
fa1573ee13 | ||
|
|
50f7866a0b | ||
|
|
7b1a1e893e | ||
|
|
40afea3908 | ||
|
|
e1ae260fc5 | ||
|
|
c33399b91d | ||
|
|
f46202223a | ||
|
|
0867573f2f | ||
|
|
2becb3aa8f | ||
|
|
dc2f4dd040 | ||
|
|
2cf3c142da | ||
|
|
a8d84fd38a | ||
|
|
4a207763cc | ||
|
|
b1ef5c33db | ||
|
|
578532efdf | ||
|
|
95fb8baaaa | ||
|
|
73e432b2dc | ||
|
|
f43c3171b0 | ||
|
|
364ade9181 | ||
|
|
8883c87dfb | ||
|
|
8e35b39197 | ||
|
|
79fd941b4e | ||
|
|
b317407bfe | ||
|
|
885630b5db | ||
|
|
cc64f5c877 | ||
|
|
7d358e0c00 | ||
|
|
eacfee78cc | ||
|
|
d4a773fc2c | ||
|
|
540124cabf | ||
|
|
6db2b33576 | ||
|
|
a132bfea65 | ||
|
|
d9f929ec63 | ||
|
|
f6f00bec3b | ||
|
|
798f8623d4 | ||
|
|
27174c05ab | ||
|
|
b8b95babe0 | ||
|
|
741b514441 | ||
|
|
f8493f2ff6 | ||
|
|
6f15026495 | ||
|
|
b9acaef46b | ||
|
|
c0d8b9941d | ||
|
|
e44b52d357 | ||
|
|
1b79662113 | ||
|
|
eb2eadf14d | ||
|
|
175760cae6 | ||
|
|
486dc67f94 | ||
|
|
1609562499 | ||
|
|
31429fb5f5 | ||
|
|
ad7e9ea5ba | ||
|
|
4c672a0ebe | ||
|
|
05a2e3942c | ||
|
|
fabb087874 | ||
|
|
c266fedd89 | ||
|
|
e64893c26c | ||
|
|
2016117d47 | ||
|
|
7fd2b9d678 | ||
|
|
1d5c5162e2 | ||
|
|
6407e1920f | ||
|
|
3bdc0f1171 | ||
|
|
a0f976f075 | ||
|
|
35104ce429 | ||
|
|
ce43c1b2c0 | ||
|
|
00cc482342 | ||
|
|
9e8521fa10 | ||
|
|
cdea2106b3 | ||
|
|
7cf03da0ee | ||
|
|
cb8f677cdf | ||
|
|
771d82e35f | ||
|
|
670dea6924 | ||
|
|
d8cfdc2123 | ||
|
|
ad8ceff2a8 | ||
|
|
1e93c0786f | ||
|
|
152ad6c973 | ||
|
|
f51dd0b0cb | ||
|
|
b06c00283d | ||
|
|
85fbb283c3 | ||
|
|
4cbedc7034 | ||
|
|
1e9dd71a7a | ||
|
|
8d9f5ba302 | ||
|
|
5e18ea163f | ||
|
|
2f7a5acf42 | ||
|
|
99cc429779 | ||
|
|
f0335b485e | ||
|
|
fc8f935092 | ||
|
|
d5cf51b5da | ||
|
|
1ae5143fb7 | ||
|
|
ac284ba71a | ||
|
|
bf68e380bc | ||
|
|
d87800f370 | ||
|
|
d65db96447 | ||
|
|
b2e344c523 | ||
|
|
2b9d7d2818 | ||
|
|
d79c2d34a5 | ||
|
|
586aafe1f1 | ||
|
|
6cb017af1c | ||
|
|
25462e38bd | ||
|
|
4ba6c365a5 | ||
|
|
aa5d229687 | ||
|
|
022370f799 | ||
|
|
050470453a | ||
|
|
5a2353fb11 | ||
|
|
c73769750c | ||
|
|
8cda9c06a2 | ||
|
|
6d16ff234a | ||
|
|
220fbe2be2 | ||
|
|
64d924d8a4 | ||
|
|
da2615096e | ||
|
|
87f2997ce8 | ||
|
|
467943ec49 | ||
|
|
902147cbf6 | ||
|
|
b165969598 | ||
|
|
39ab7558f9 | ||
|
|
600c4d32ab | ||
|
|
00b145bcf9 | ||
|
|
4c15d64ece | ||
|
|
a639a2581a | ||
|
|
51cd53ee9e | ||
|
|
7ba94b9315 | ||
|
|
28275bb6d9 | ||
|
|
953e45f62e | ||
|
|
709514ff3c | ||
|
|
12940d46d3 | ||
|
|
8f4b6a5d1b | ||
|
|
d46e582c91 | ||
|
|
7bbf986c09 | ||
|
|
2e6d5c87bc | ||
|
|
0e4d0b0f84 | ||
|
|
533362210b | ||
|
|
014064376c | ||
|
|
b26ddb809c | ||
|
|
e9a95fcc53 | ||
|
|
b3ddf94089 | ||
|
|
2213ab94da | ||
|
|
62d95d73e1 | ||
|
|
22a1fc089e | ||
|
|
9f95157c18 | ||
|
|
dec69e959d | ||
|
|
81a52dcc0e | ||
|
|
dd3818499c | ||
|
|
b141196384 | ||
|
|
2dfe8c64e5 | ||
|
|
033a513c92 | ||
|
|
0bbb504511 | ||
|
|
88fe86c19a | ||
|
|
3ccf239d84 | ||
|
|
c6ef654c87 | ||
|
|
4ef0373d31 | ||
|
|
71f91b8050 | ||
|
|
6d8315ac4e | ||
|
|
08e550f46f | ||
|
|
dfa67a00e3 | ||
|
|
cdd58773de | ||
|
|
808ecb865b | ||
|
|
277ff6c012 | ||
|
|
836f7a311e | ||
|
|
a3b232543f | ||
|
|
6077f8f377 | ||
|
|
62711f603b | ||
|
|
a59fa22fa6 | ||
|
|
cc10bcdfb8 | ||
|
|
481c283c36 | ||
|
|
947988c2ce | ||
|
|
202df3d9c3 | ||
|
|
8f3ad3d171 | ||
|
|
aa771ae1b2 | ||
|
|
158a526aee | ||
|
|
8c4e078490 | ||
|
|
98dea2c4bf | ||
|
|
db62eeec22 | ||
|
|
af1e813c48 | ||
|
|
d57ac9e743 | ||
|
|
f5e02fb784 | ||
|
|
96a7fbaf3b | ||
|
|
c749853870 | ||
|
|
8c93fceb3e | ||
|
|
fb0ef1c59a | ||
|
|
847d97a0e9 | ||
|
|
8d9c80ef61 | ||
|
|
c22ba0c2cf | ||
|
|
5e4654a968 | ||
|
|
bbf08d16d4 | ||
|
|
19c6296a86 | ||
|
|
b8ffd39f99 | ||
|
|
fe29cb7a2c | ||
|
|
4137cc4736 | ||
|
|
3d23731f0e | ||
|
|
bcbda92601 | ||
|
|
f0fca573fd | ||
|
|
4abc674970 | ||
|
|
43ed35a1be | ||
|
|
689ab0b388 | ||
|
|
a884895fae | ||
|
|
a644df1e3c | ||
|
|
48abe09415 | ||
|
|
f1bc79a9a4 | ||
|
|
ce61fc36e1 | ||
|
|
cbce527aa1 | ||
|
|
53ea7c2477 | ||
|
|
aae0846639 | ||
|
|
c2648cf2cb | ||
|
|
68c19957e0 | ||
|
|
6c79503d1f | ||
|
|
c5fc5c0e81 | ||
|
|
4170e754ea | ||
|
|
5d44a3aeff | ||
|
|
8243213028 | ||
|
|
efc422ac44 | ||
|
|
497430f729 | ||
|
|
659dc7b55d | ||
|
|
581fd945c2 | ||
|
|
8205fa9d6e | ||
|
|
cc4a6db976 | ||
|
|
efb7ae009d | ||
|
|
f29606ea94 | ||
|
|
4a586cf117 | ||
|
|
64688cd2b5 | ||
|
|
ea9c6e9aa7 | ||
|
|
889dc81404 | ||
|
|
df85846fbe | ||
|
|
5a9632f80e | ||
|
|
9105215dc8 | ||
|
|
4aa57f95b5 | ||
|
|
1112922731 | ||
|
|
2bee131ff4 | ||
|
|
75933efbdd | ||
|
|
93623a2f05 | ||
|
|
4b12518ee4 | ||
|
|
2a834eeb38 | ||
|
|
25872f08de | ||
|
|
9fc0f4d7da | ||
|
|
4e2bf10115 | ||
|
|
957a9474ec | ||
|
|
bd0d4ad2a4 | ||
|
|
88fa8a0c17 | ||
|
|
943f16789f | ||
|
|
bb91637db5 | ||
|
|
5aef4c58e2 | ||
|
|
554ea91bda | ||
|
|
d5a858d78d | ||
|
|
ff265c3a86 | ||
|
|
f2fced86b2 | ||
|
|
83f62e17b2 | ||
|
|
0f80217e74 | ||
|
|
7290ee870c | ||
|
|
27c0c9194e | ||
|
|
ae652297fa | ||
|
|
bcdb9efee8 | ||
|
|
f224507f91 | ||
|
|
dc9ba64b21 | ||
|
|
c99394416e | ||
|
|
ba2abde97d | ||
|
|
66b33a8686 | ||
|
|
1ebdce5216 | ||
|
|
c2d1ea9895 | ||
|
|
75a9278d56 | ||
|
|
b508354ac6 | ||
|
|
e1478c055f | ||
|
|
c07f0c33bb | ||
|
|
4e2b10eeab | ||
|
|
05e9285752 | ||
|
|
a76a21a935 | ||
|
|
0fd23eab59 | ||
|
|
1c1d1e1d74 | ||
|
|
9412f862eb | ||
|
|
9f7ba2eb20 | ||
|
|
101d1d485a | ||
|
|
e316836ee5 | ||
|
|
c6e3c41759 |
@@ -25,11 +25,6 @@ csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
dotnet_diagnostic.SA1011.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
dotnet_diagnostic.SA1310.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = none
|
||||
|
||||
# Razor files
|
||||
[*.razor]
|
||||
@@ -65,59 +60,3 @@ indent_size = 4
|
||||
[*.xml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = false:suggestion
|
||||
dotnet_style_null_propagation = false:suggestion
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0046.severity = silent
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
HOSTNAME=
|
||||
JWT_KEY=
|
||||
DATA_PROTECTION_CERT_PASS=
|
||||
ADMIN_PASSWORD_HASH=
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
SMTP_TLS_ENABLED=false
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
53
.github/dependabot.yml
vendored
Normal file
53
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Enable version updates for NuGet
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/"
|
||||
target-branch: "main"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "dependencies"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
interval: "weekly"
|
||||
# Ignore certain dependencies (optional)
|
||||
# ignore:
|
||||
# - dependency-name: "SomePackage"
|
||||
# versions: ["4.x", "5.x"]
|
||||
|
||||
# Enable version updates for npm
|
||||
- package-ecosystem: "npm"
|
||||
# Look for `package.json` and `lock` files in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
interval: "weekly"
|
||||
|
||||
# Enable version updates for Docker
|
||||
- package-ecosystem: "docker"
|
||||
# Look for a `Dockerfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
interval: "weekly"
|
||||
|
||||
# Enable version updates for Composer
|
||||
- package-ecosystem: "composer"
|
||||
# Look for a `Dockerfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
interval: "weekly"
|
||||
6
.github/release.yml
vendored
6
.github/release.yml
vendored
@@ -9,9 +9,9 @@ changelog:
|
||||
labels:
|
||||
- dependencies
|
||||
- bug
|
||||
- title: 🧩 Dependencies Updates
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🐞 Bug Fixes
|
||||
labels:
|
||||
- bug
|
||||
- title: 🧩 Dependencies Updates
|
||||
labels:
|
||||
- dependencies
|
||||
|
||||
97
.github/workflows/docker-compose-build.yml
vendored
97
.github/workflows/docker-compose-build.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# This workflow will test if building the Docker Compose containers from scratch works.
|
||||
name: Docker Compose Build
|
||||
|
||||
on:
|
||||
@@ -9,37 +10,91 @@ on:
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
docker:
|
||||
image: docker:26.0.0
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set permissions and run install.sh
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh build --verbose
|
||||
|
||||
- name: Set up Docker Compose
|
||||
run: |
|
||||
# Build the images and start the services
|
||||
# Change the exposed host port of the SmtpService from 25 to 2525 because port 25 is not allowed in GitHub Actions
|
||||
sed -i 's/25\:25/2525\:25/g' docker-compose.yml
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
run: |
|
||||
# Wait for a few seconds
|
||||
sleep 5
|
||||
- name: Test if localhost:80 (WASM app) responds
|
||||
sleep 10
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/api (WebApi) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/api)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with expected 200 OK. Check if WebApi and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/admin (Admin) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/admin/user/login)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with expected 200 OK. Check if admin app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:2525 (SmtpService) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
if ! nc -zv localhost 2525 2>&1 | grep -q 'succeeded'; then
|
||||
echo "SmtpService did not respond on port 2525. Check if the SmtpService service is running."
|
||||
exit 1
|
||||
else
|
||||
echo "SmtpService responded on port 2525"
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
run: |
|
||||
# Test if the service on localhost:80 responds
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK"
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
- name: Test if localhost:81 (WebApi) responds
|
||||
run: |
|
||||
# Test if the service on localhost:81 responds
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:81)
|
||||
if [ "$http_code" -ne 200 ] && [ "$http_code" -ne 404 ]; then
|
||||
echo "Service did not respond with expected 200 OK or 404 Not Found"
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
fi
|
||||
output=$(./install.sh reset-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual output: $output"
|
||||
exit 1
|
||||
else
|
||||
echo "Password reset output format is correct"
|
||||
fi
|
||||
|
||||
100
.github/workflows/docker-compose-pull.yml
vendored
Normal file
100
.github/workflows/docker-compose-pull.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
# This workflow will test if pulling the latest Docker Compose containers from the registry works.
|
||||
name: Docker Compose Pull
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
docker:
|
||||
image: docker:26.0.0
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set permissions and run install.sh
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh install --verbose
|
||||
|
||||
- name: Set up Docker Compose
|
||||
run: |
|
||||
# Change the exposed host port of the SmtpService from 25 to 2525 because port 25 is not allowed in GitHub Actions
|
||||
sed -i 's/25\:25/2525\:25/g' docker-compose.yml
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
run: |
|
||||
# Wait for a few seconds
|
||||
sleep 10
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/api (WebApi) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/api)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with expected 200 OK. Check if WebApi and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/admin (Admin) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/admin/user/login)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with expected 200 OK. Check if admin app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:2525 (SmtpService) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
if ! nc -zv localhost 2525 2>&1 | grep -q 'succeeded'; then
|
||||
echo "SmtpService did not respond on port 2525. Check if the SmtpService service is running."
|
||||
exit 1
|
||||
else
|
||||
echo "SmtpService responded on port 2525"
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual output: $output"
|
||||
exit 1
|
||||
else
|
||||
echo "Password reset output format is correct"
|
||||
fi
|
||||
43
.github/workflows/dotnet-e2e-admin-tests.yml
vendored
Normal file
43
.github/workflows/dotnet-e2e-admin-tests.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# This workflow will test if running the E2E Admin tests via Playwright CLI works.
|
||||
name: .NET E2E Admin Tests (Playwright)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
admin-tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
- name: Ensure browsers are installed
|
||||
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
|
||||
|
||||
- name: Run AdminTests with retry
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 60
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=AdminTests"
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: admin-test-results
|
||||
path: TestResults-Admin.xml
|
||||
40
.github/workflows/dotnet-e2e-client-tests.yml
vendored
Normal file
40
.github/workflows/dotnet-e2e-client-tests.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# This workflow will test if running the E2E Client tests via Playwright CLI works.
|
||||
name: .NET E2E Client Tests (Playwright with Sharding)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
client-tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
- name: Ensure browsers are installed
|
||||
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
|
||||
|
||||
- name: Run ClientTests with retry (Shard ${{ matrix.shard }})
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 60
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "FullyQualifiedName~.E2ETests.Tests.Client.Shard${{ matrix.shard }}."
|
||||
43
.github/workflows/dotnet-e2e-misc-tests.yml
vendored
Normal file
43
.github/workflows/dotnet-e2e-misc-tests.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# This workflow will test if running the E2E Misc tests via Playwright CLI works.
|
||||
name: .NET E2E Misc Tests (Playwright)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
misc-tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
- name: Ensure browsers are installed
|
||||
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
|
||||
|
||||
- name: Run remaining tests with retry
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 60
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category!=AdminTests&Category!=ClientTests"
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: misc-test-results
|
||||
path: TestResults-Misc.xml
|
||||
20
.github/workflows/dotnet-integration-tests.yml
vendored
20
.github/workflows/dotnet-integration-tests.yml
vendored
@@ -1,7 +1,5 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
|
||||
name: Playwright integration tests
|
||||
# This workflow will test if running the integration tests works.
|
||||
name: .NET Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -15,13 +13,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
- name: Ensure browsers are installed
|
||||
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
|
||||
- name: Run your tests
|
||||
run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal
|
||||
|
||||
- name: Run integration tests
|
||||
run: dotnet test src/Tests/AliasVault.IntegrationTests --no-build --verbosity normal
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
|
||||
name: .NET build and run tests
|
||||
# This workflow will test if running the unit tests works.
|
||||
name: .NET Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,19 +8,24 @@ on:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --no-restore
|
||||
- name: Test
|
||||
|
||||
- name: Run unittests
|
||||
run: dotnet test src/Tests/AliasVault.UnitTests --no-build --verbosity normal
|
||||
87
.github/workflows/publish-docker-images.yml
vendored
Normal file
87
.github/workflows/publish-docker-images.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# This workflow will publish new Docker images to the GitHub Container Registry when a new release is published.
|
||||
name: Publish Docker Images
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
|
||||
|
||||
- name: Build and push API image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Api/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Client image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Client/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Admin image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Admin/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push SMTP image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Reverse Proxy image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push InstallCli image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
16
.github/workflows/sonarcloud-code-analysis.yml
vendored
16
.github/workflows/sonarcloud-code-analysis.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# This workflow will perform a SonarCloud code analysis on every push to the main branch or when a pull request is opened, synchronized, or reopened.
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
@@ -10,20 +11,31 @@ jobs:
|
||||
name: Build and analyze
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Install WASM workload
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
@@ -31,15 +43,17 @@ jobs:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -268,6 +268,7 @@ ServiceFabricBackup/
|
||||
|
||||
# SQLite files
|
||||
*.sqlite
|
||||
*.sqlite.*
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
|
||||
@@ -371,6 +372,30 @@ FodyWeavers.xsd
|
||||
.idea
|
||||
*.licenseheader
|
||||
|
||||
# AliasVault specific
|
||||
# Codebuddy Rider plugin
|
||||
.codebuddy
|
||||
|
||||
# -------------------
|
||||
# AliasVault specifics
|
||||
# -------------------
|
||||
# index.html is generated by the build process from index.template.html and therefore should be ignored
|
||||
src/AliasVault.WebApp/wwwroot/index.html
|
||||
src/AliasVault.Client/wwwroot/index.html
|
||||
|
||||
# appsettings.Development.json is generated by the build process from appsettings.Development.template.json and therefore should be ignored
|
||||
src/AliasVault.Client/wwwroot/appsettings.Development.json
|
||||
|
||||
# appsettings.Development.json is added manually if needed, it should not be committed.
|
||||
src/Tests/AliasVault.E2ETests/appsettings.Development.json
|
||||
|
||||
# .env is generated by install.sh and therefore should be ignored
|
||||
.env
|
||||
|
||||
# Draw.io diagram temp files
|
||||
*.drawio.*
|
||||
|
||||
# Certificates
|
||||
certificates/**/*.crt
|
||||
certificates/**/*.key
|
||||
certificates/**/*.pfx
|
||||
certificates/**/*.pem
|
||||
certificates/letsencrypt/**
|
||||
|
||||
57
.globalconfig
Normal file
57
.globalconfig
Normal file
@@ -0,0 +1,57 @@
|
||||
dotnet_diagnostic.SA1011.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
dotnet_diagnostic.SA1310.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = none
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
|
||||
dotnet_style_coalesce_expression = false:suggestion
|
||||
dotnet_style_null_propagation = false:suggestion
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0046.severity = silent
|
||||
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "C#: AliasVault.WebApp [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.WebApp/AliasVault.WebApp.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
},
|
||||
{
|
||||
"name": "C#: AliasVault.Api [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.Api/AliasVault.Api.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -68,3 +68,34 @@ dotnet tool install --global Microsoft.Playwright.CLI
|
||||
# Note: make sure the E2E test project has been built at least once so the bin dir exists.
|
||||
pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install
|
||||
```
|
||||
|
||||
### 7. Create AliasVault.Client appsettings.Development.json
|
||||
The WASM client app supports a development specific appsettings.json file. This appsettings file is optional but can override various options to make debugging easier.
|
||||
|
||||
|
||||
1. Copy `wwwroot/appsettings.json` to `wwwroot/appsettings.Development.json`
|
||||
|
||||
Here is an example file with the various options explained:
|
||||
|
||||
```
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"PrivateEmailDomains": ["example.tld"],
|
||||
"SupportEmail": "support@example.tld",
|
||||
"UseDebugEncryptionKey": "true",
|
||||
"CryptographyOverrideType" : "Argon2Id",
|
||||
"CryptographyOverrideSettings" : "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
|
||||
}
|
||||
```
|
||||
|
||||
- UseDebugEncryptionKey
|
||||
- This setting will use a static encryption key so that if you login as a user you can refresh the page without needing to unlock the database again. This speeds up development when changing things in the WebApp WASM project. Note: the project needs to be run in "Development" mode for this setting to be used.
|
||||
|
||||
- CryptographyOverrideType
|
||||
- This setting allows overriding the default encryption type (Argon2id) with a different encryption type. This is useful for testing different encryption types without having to change code.
|
||||
|
||||
- CryptographyOverrideSettings
|
||||
- This setting allows overriding the default encryption settings (Argon2id) with different settings. This is useful for testing different encryption settings without having to change code. The default Argon2id settings
|
||||
are defined in the project as `Utilities/Cryptography/Cryptography.Client/Defaults.cs`. These default settings
|
||||
are focused on security but NOT performance. Normally for key derivation purposes the slower/heavier the algorithm
|
||||
the better protection against attackers. For production builds this is what we want, however in case of automated testing or debugging extra performance can be gained by tweaking (lowering) these settings.
|
||||
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
# Install OpenSSL
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
# Copy configuration and entrypoint script
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
# Create SSL directory
|
||||
RUN mkdir -p /etc/nginx/ssl && chmod 755 /etc/nginx/ssl \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
120
README.md
120
README.md
@@ -1,60 +1,110 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>AliasVault</h1>
|
||||
<h1><img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="40" /> AliasVault</h1>
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
|
||||
<p align="center">
|
||||
<a href="https://app.aliasvault.net">Live demo 🚀</a> • <a href="https://aliasvault.net?utm_source=gh-readme">Website 🏠</a> • <a href="#installation">Installation 📦</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
Open-source password and alias manager
|
||||
</h3>
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-build-run-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-integration-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-unit-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=integration tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-e2e-client-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-client-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/coverage/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=test code coverage">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
</div>
|
||||
|
||||
> Disclaimer: This repository is currently in an alpha state and is NOT ready for production use. Critical features, such as encryption, are not yet fully implemented. AliasVault is a work in progress and as of this moment serves as a research playground. Users are welcome to explore and use this project, but please be aware that there are no guarantees regarding its security or stability. Use at your own risk!
|
||||
AliasVault is an open-source password and alias manager built with C# ASP.NET technology. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
|
||||
AliasVault is an open-source password manager that can generate virtual identities complete with virtual email addresses. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
### What makes AliasVault unique:
|
||||
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
|
||||
- **Built-in email server**: AliasVault includes its own email server that allows you to generate virtual email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app.
|
||||
- **Alias generation**: Generate aliases and assign them to a website, allowing you to use different email addresses and usernames for each website. Keeping your online identities separate and secure, making it harder for bad actors to link your accounts.
|
||||
- **Open-source**: The source code is available on GitHub and can be self-hosted on your own server.
|
||||
|
||||
## Features
|
||||
- **Password Management:** Securely store and manage your passwords.
|
||||
- **Virtual Identities:** Generate virtual identities with virtual (working) email addresses that are assigned to one or more passwords.
|
||||
- **Zero-knowledge architecture:** Ensures that all sensitive data is end-to-end encrypted on the client and stored in encrypted state on the database. The server never has access to your data.
|
||||
> Note: AliasVault is currently in active development and some features may not yet have been (fully) implemented. If you run into any issues, please create an issue on GitHub.
|
||||
|
||||
## Live demo
|
||||
A live demo of the app is available at the official website at [app.aliasvault.net](https://app.aliasvault.net) (up-to-date with `main` branch). You can create a free account to try it out yourself.
|
||||
|
||||
<img width="700" alt="Screenshot of AliasVault" src="docs/img/screenshot.png">
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Clone this repository.
|
||||
Choose one of the following installation methods:
|
||||
|
||||
### Option 1: Quick Install (Pre-built Images)
|
||||
|
||||
This method uses pre-built Docker images and works on minimal hardware specifications:
|
||||
- Linux (Ubuntu or RHEL based distros recommended)
|
||||
- 512MB RAM
|
||||
- 1 vCPU
|
||||
- At least 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
```bash
|
||||
# Clone this Git repository to "AliasVault" directory
|
||||
$ git clone https://github.com/lanedirt/AliasVault.git
|
||||
# Download install script
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
./install.sh install
|
||||
```
|
||||
|
||||
### 2. Run the init script to set up the .env file and generate a random encryption secret.
|
||||
This script will create a .env file in the root directory of the project if it does not yet exist and populate it with a random encryption secret.
|
||||
```bash
|
||||
# Go to the project directory
|
||||
$ cd AliasVault
|
||||
### Option 2: Build from Source
|
||||
|
||||
# Make init script executable
|
||||
$ chmod +x init.sh
|
||||
|
||||
# Run the init script
|
||||
$ ./init.sh
|
||||
```
|
||||
|
||||
### 3. Build and run the app via Docker:
|
||||
Building from source requires more resources:
|
||||
- Minimum 2GB RAM (more RAM will speed up build time)
|
||||
- At least 1 vCPU
|
||||
- 40GB+ disk space (for dependencies and build artifacts)
|
||||
- Docker installed
|
||||
- Git installed
|
||||
|
||||
```bash
|
||||
# Build and run the app via Docker Compose
|
||||
$ docker compose up -d --build --force-recreate
|
||||
# Clone the repository
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
cd AliasVault
|
||||
|
||||
# Make build script executable and run it. This will create the .env file, build the Docker images from source, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
./install.sh build
|
||||
```
|
||||
> Note: the container binds to port 80 by default. If you have another service running on port 80, you can change the port in the `docker-compose.yml` file.
|
||||
|
||||
Note: If you do not wish to run the script, you can set up the environment variables and build the Docker image and containers manually instead. See the [manual setup instructions](docs/install/1-manually-setup-docker.md) for more information.
|
||||
|
||||
#### Note for first time build:
|
||||
- When running the app for the first time, it may take a few minutes to build the Docker image.
|
||||
- A SQLite database file will be created in `./database/aliasdb.sqlite`. This file will store all (encrypted) password vaults. It should be kept secure and not shared.
|
||||
### Post-Installation
|
||||
|
||||
After the Docker containers have started the app will be available at http://localhost:80
|
||||
The install script will output the URL where the app is available. By default this is:
|
||||
- Client: https://localhost
|
||||
- Admin portal: https://localhost/admin
|
||||
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `nginx` (reverse-proxy) container.
|
||||
|
||||
#### First Time Setup Notes:
|
||||
- When building from source for the first time, it may take several minutes for Docker to download and compile all dependencies. Subsequent builds will be faster.
|
||||
- A SQLite database file will be created in `./database/AliasServerDb.sqlite`. This file will store all (encrypted) password vaults. It should be kept secure and not shared.
|
||||
|
||||
#### Useful Commands:
|
||||
- To reset the admin password: `./install.sh reset-password`
|
||||
- To uninstall AliasVault: `./install.sh uninstall`
|
||||
This will remove all containers, images, and volumes related to AliasVault while keeping configuration files intact for future reinstallation.
|
||||
- If something goes wrong you can run the install script in verbose mode to get more information: `./install.sh [command] --verbose`
|
||||
|
||||
## Security Architecture
|
||||
AliasVault takes security seriously and implements various measures to protect your data:
|
||||
|
||||
- All sensitive user data is encrypted end-to-end using industry-standard encryption algorithms. This includes the complete vault contents and all received emails.
|
||||
- Your master password never leaves your device.
|
||||
- Zero-knowledge architecture ensures the server never has access to your unencrypted data
|
||||
|
||||
For detailed information about our encryption implementation and security architecture, see the following documents:
|
||||
- [SECURITY.md](SECURITY.md)
|
||||
- [Security Architecture Diagram](docs/security-architecture.md)
|
||||
|
||||
## Tech stack / credits
|
||||
The following technologies, frameworks and libraries are used in this project:
|
||||
@@ -63,8 +113,12 @@ The following technologies, frameworks and libraries are used in this project:
|
||||
- [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) - An open-source framework for building modern, cloud-based, internet-connected applications.
|
||||
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) - A lightweight, extensible, open-source and cross-platform version of the popular Entity Framework data access technology.
|
||||
- [Blazor WASM](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript. It's a single-page app framework that runs in the browser via WebAssembly.
|
||||
- [Playwright](https://playwright.dev/) - A Node.js library to automate Chromium, Firefox and WebKit with a single API. Used for end-to-end testing.
|
||||
- [Docker](https://www.docker.com/) - A platform for building, sharing, and running containerized applications.
|
||||
- [SQLite](https://www.sqlite.org/index.html) - A C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine.
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library based on Tailwind CSS.
|
||||
- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm.
|
||||
- [SRP.net](https://github.com/secure-remote-password/srp.net) - SRP6a Secure Remote Password protocol for secure password authentication.
|
||||
- [SmtpServer](https://github.com/cosullivan/SmtpServer) - A SMTP server library for .NET that is used for the virtual email address feature.
|
||||
- [MimeKit](https://github.com/jstedfast/MimeKit) - A .NET MIME creation and parser library used for the virtual email address feature.
|
||||
|
||||
95
SECURITY.md
Normal file
95
SECURITY.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# SECURITY.md
|
||||
This document describes the encryption algorithms used by AliasVault in order to keep its user data secure.
|
||||
|
||||
## Overview
|
||||
AliasVault features a [zero-knowledge architecture](https://en.wikipedia.org/wiki/Zero-knowledge_service) and uses a combination of encryption algorithms to protect the data of its users.
|
||||
|
||||
The basic premise is that the master password chosen by the user upon registration forms the basis for all encryption
|
||||
and decryption operations. This master password is never transmitted over the network and only resides on the client.
|
||||
All data is encrypted at rest and in transit. This ensures that even if the AliasVault servers are compromised,
|
||||
the user's data remains secure.
|
||||
|
||||
## Encryption algorithms
|
||||
The following encryption algorithms are used by AliasVault:
|
||||
|
||||
- [Argon2id](#argon2id)
|
||||
- [SRP](#srp)
|
||||
- [AES-GCM](#aes-gcm)
|
||||
- [RSA-OAEP](#rsa-oaep)
|
||||
|
||||
Below is a detailed explanation of each encryption algorithm.
|
||||
|
||||
For more information about how these algorithms are specifically used in AliasVault, see the [Security Architecture](docs/security-architecture.md) document.
|
||||
|
||||
### Argon2id
|
||||
To derive a key from the master password, AliasVault uses the Argon2id key derivation function. Argon2id is a memory-hard
|
||||
key derivation function which allows for controlling the execution time, memory required and degree of parallelism.
|
||||
This makes it resilient against brute-force attacks and makes it one of the best choices for deriving keys from passwords.
|
||||
|
||||
AliasVault uses Argon2id with the following default parameters:
|
||||
- Degree of parallelism: 1
|
||||
- Memory size: 19456 KB
|
||||
- Iterations: 2
|
||||
|
||||
More information about Argon2id can be found on the [Argon2](https://en.wikipedia.org/wiki/Argon2) Wikipedia page.
|
||||
|
||||
### SRP
|
||||
The Secure Remote Password (SRP) protocol is used for authenticating a user with the AliasVault server during login.
|
||||
The SRP protocol is a password-authenticated key exchange protocol (PAKE). This means that the client and server can
|
||||
authenticate each other using a password, without sending the password itself over the network.
|
||||
|
||||
With the use of SRP the master password never leaves the client. The client sends a verifier to the server,
|
||||
which is a value derived from the master password. The server uses this verifier to authenticate the client without
|
||||
having ever seen the actual master password.
|
||||
|
||||
For more information see the [SRP protocol](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) information on Wikipedia.
|
||||
|
||||
### AES-256-GCM
|
||||
All user's vault data is fully encrypted on the client using the AES-256-GCM encryption algorithm, which stands for
|
||||
*Advanced Encryption Standard with 256-bit key in Galois/Counter Mode*. The key for encryption is derived from the
|
||||
master password by using the Argon2Id algorithm. AliasVault implements AES-GCM with the following specifications:
|
||||
|
||||
- Key Size: 256 bits
|
||||
- Uses the Web Crypto API's SubtleCrypto interface for secure cryptographic operations
|
||||
- Generates a random 12-byte (96-bit) IV (initialization vector) for each encryption operation
|
||||
- Performs all encryption/decryption operations entirely in the browser
|
||||
|
||||
#### The encryption process works as follows:
|
||||
- A unique IV is generated for each encryption operation
|
||||
- The users vault data is encrypted using AES-GCM with the derived key and IV
|
||||
- The IV is prepended to the ciphertext
|
||||
|
||||
More information about AES-GCM can be found on the [AES-GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) Wikipedia page.
|
||||
|
||||
### RSA-OAEP
|
||||
To secure email communications, AliasVault uses RSA-OAEP (RSA with Optimal Asymmetric Encryption Padding). This asymmetric
|
||||
encryption system allows AliasVault to store emails on the server in encrypted state which can only be read by the
|
||||
intended recipient. AliasVault implements RSA-OAEP with the following specifications:
|
||||
- Algorithm: RSA-OAEP with SHA-256 hash
|
||||
- Key Size: 2048-bit modulus
|
||||
- Key Format: JWK (JSON Web Key)
|
||||
- Padding: OAEP (Optimal Asymmetric Encryption Padding)
|
||||
|
||||
#### Email Security Flow
|
||||
1. Key Generation: When a user creates a vault, a RSA key pair is generated:
|
||||
- A private key that remains in the encrypted user's vault and is never transmitted
|
||||
- A public key that is sent to the server
|
||||
|
||||
2. Email Reception Process: When an email arrives at the AliasVault email server:
|
||||
- The server generates a random 256-bit symmetric encryption key to encrypt the email contents
|
||||
- The symmetric encryption key is encrypted using the recipient's asymmetric public key
|
||||
- The encrypted email contents together with the encrypted symmetric encryption key are stored in the server's database
|
||||
- The original email content is never stored or logged
|
||||
|
||||
3. Email Retrieval Process:
|
||||
- When a user accesses their emails, the encrypted content is retrieved from the server
|
||||
- The client-side application decrypts the symmetric encryption key using the user's private key that is stored in their vault
|
||||
- The decrypted symmetric encryption key is used to decrypt the email contents
|
||||
- Decryption occurs entirely in the browser, maintaining end-to-end encryption
|
||||
|
||||
This implementation ensures that:
|
||||
- Emails are encrypted and secure at rest in the server database
|
||||
- Only the intended recipient that holds the private key can decrypt and read their emails
|
||||
- Even if the server is compromised, email contents remain encrypted and unreadable
|
||||
|
||||
More information about RSA-OAEP can be found on the [RSA-OAEP](https://en.wikipedia.org/wiki/Optimal_asymmetric_encryption_padding) Wikipedia page.
|
||||
174
aliasvault.sln
174
aliasvault.sln
@@ -3,48 +3,76 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.10.34928.147
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault", "src\AliasVault\AliasVault.csproj", "{BD2050C0-DC26-4777-9514-546525307370}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasDb", "src\AliasDb\AliasDb.csproj", "{64F47C9A-FE69-4793-B469-28BAADEC6706}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasGenerators", "src\AliasGenerators\AliasGenerators.csproj", "{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FaviconExtractor", "src\Utilities\FaviconExtractor\FaviconExtractor.csproj", "{ED328644-A152-403D-86EB-81201AA07744}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.FaviconExtractor", "src\Utilities\AliasVault.FaviconExtractor\AliasVault.FaviconExtractor.csproj", "{ED328644-A152-403D-86EB-81201AA07744}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.UnitTests", "src\Tests\AliasVault.UnitTests\AliasVault.UnitTests.csproj", "{8E6A418A-B305-465D-857D-49953605C18E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "src\Utilities\Cryptography\Cryptography.csproj", "{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Api", "src\AliasVault.Api\AliasVault.Api.csproj", "{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.WebApp", "src\AliasVault.WebApp\AliasVault.WebApp.csproj", "{25248E01-5A4B-4F95-A63C-BEA01499A1C2}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Client", "src\AliasVault.Client\AliasVault.Client.csproj", "{25248E01-5A4B-4F95-A63C-BEA01499A1C2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Shared", "src\AliasVault.Shared\AliasVault.Shared.csproj", "{15EFE0D0-F41B-47D7-86B7-8F840335CB82}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.Shared", "src\Shared\AliasVault.Shared\AliasVault.Shared.csproj", "{15EFE0D0-F41B-47D7-86B7-8F840335CB82}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests", "src\Tests\AliasVault.E2ETests\AliasVault.E2ETests.csproj", "{AF013D08-1BF6-4E23-87D2-37F614BE7952}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{5F7417F6-4388-49CC-9511-ED63C4A6488A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasServerDb", "src\Databases\AliasServerDb\AliasServerDb.csproj", "{1277105D-50CD-4CE0-9C2C-549F46867E54}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasClientDb", "src\Databases\AliasClientDb\AliasClientDb.csproj", "{FE10F294-817F-477E-A24F-8597A15AF0B5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests.Client.Server", "src\Tests\Server\AliasVault.E2ETests.Client.Server\AliasVault.E2ETests.Client.Server.csproj", "{DD1F496F-CF10-47D1-A57F-5FA256479332}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{607945F3-9896-4544-99EC-F3496CF4D36B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.CsvImportExport", "src\Utilities\AliasVault.CsvImportExport\AliasVault.CsvImportExport.csproj", "{A9C9A606-C87E-4298-AB32-09B1884D7487}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{8A477241-B96C-4174-968D-D40CB77F1ECD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.SmtpService", "src\Services\AliasVault.SmtpService\AliasVault.SmtpService.csproj", "{B095A174-E528-4D38-BEC1-D1D38B3B30C0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.IntegrationTests", "src\Tests\AliasVault.IntegrationTests\AliasVault.IntegrationTests.csproj", "{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Admin", "src\AliasVault.Admin\AliasVault.Admin.csproj", "{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.InstallCli", "src\Utilities\AliasVault.InstallCli\AliasVault.InstallCli.csproj", "{857BCD0E-753F-437A-AF75-B995B4D9A5FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Logging", "src\Utilities\AliasVault.Logging\AliasVault.Logging.csproj", "{FF0B0E64-1AE2-415C-A404-0EB78010821A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.RazorComponents", "src\Shared\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj", "{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.WorkerStatus", "src\Utilities\AliasVault.WorkerStatus\AliasVault.WorkerStatus.csproj", "{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.TotpGenerator", "src\Utilities\AliasVault.TotpGenerator\AliasVault.TotpGenerator.csproj", "{E8D9C551-67D2-4651-8EDF-4262DF7375CE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Auth", "src\Utilities\AliasVault.Auth\AliasVault.Auth.csproj", "{DA175274-0FF7-4436-9266-742F96C2D1ED}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cryptography", "Cryptography", "{BB7E701E-B1C6-453E-800A-E12CE256318D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Cryptography.Server", "src\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj", "{341EC443-0B6B-4E8C-AF46-D6156573CEA5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Cryptography.Client", "src\Utilities\Cryptography\AliasVault.Cryptography.Client\AliasVault.Cryptography.Client.csproj", "{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generators", "Generators", "{03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Password", "src\Generators\AliasVault.Generators.Password\AliasVault.Generators.Password.csproj", "{47F47A1B-49E0-406A-81C8-31FF2E4C339B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Identity", "src\Generators\AliasVault.Generators.Identity\AliasVault.Generators.Identity.csproj", "{80E74FBC-4EC8-45FB-B210-473337C484B5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DD359F0A-0180-4F8F-9E48-46213386BA4D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Shared.Core", "src\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj", "{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ED328644-A152-403D-86EB-81201AA07744}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ED328644-A152-403D-86EB-81201AA07744}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ED328644-A152-403D-86EB-81201AA07744}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -53,10 +81,6 @@ Global
|
||||
{8E6A418A-B305-465D-857D-49953605C18E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8E6A418A-B305-465D-857D-49953605C18E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8E6A418A-B305-465D-857D-49953605C18E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B797C533-260E-4DA2-83B1-0EE4BCFE08DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -73,6 +97,78 @@ Global
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B095A174-E528-4D38-BEC1-D1D38B3B30C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B095A174-E528-4D38-BEC1-D1D38B3B30C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B095A174-E528-4D38-BEC1-D1D38B3B30C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B095A174-E528-4D38-BEC1-D1D38B3B30C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F2CAE93E-94A7-4365-8E84-8D48CE8DD53F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -80,7 +176,29 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{ED328644-A152-403D-86EB-81201AA07744} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332} = {607945F3-9896-4544-99EC-F3496CF4D36B}
|
||||
{607945F3-9896-4544-99EC-F3496CF4D36B} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{B095A174-E528-4D38-BEC1-D1D38B3B30C0} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{BB7E701E-B1C6-453E-800A-E12CE256318D} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
6
certificates/README.md
Normal file
6
certificates/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Certificates directory structure
|
||||
|
||||
This directory contains certificates for AliasVault.
|
||||
|
||||
- `app`: Certificates that AliasVault uses to protect application data at rest (e.g. .NET DataProtection keys)
|
||||
- `ssl`: SSL/TLS certificates for AliasVault hosted services
|
||||
7
certificates/ssl/README.md
Normal file
7
certificates/ssl/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# SSL certificates directory structure
|
||||
|
||||
This directory contains SSL/TLS certificates for various AliasVault services:
|
||||
|
||||
- `admin`: Certificate for the Admin UI.
|
||||
- `api`: Certificate for the API service.
|
||||
- `client`: Certificate for the Client UI.
|
||||
30
docker-compose.build.yml
Normal file
30
docker-compose.build.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: aliasvault-reverse-proxy
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
client:
|
||||
image: aliasvault-client
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault.Client/Dockerfile
|
||||
|
||||
api:
|
||||
image: aliasvault-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault.Api/Dockerfile
|
||||
|
||||
admin:
|
||||
image: aliasvault-admin
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault.Admin/Dockerfile
|
||||
|
||||
smtp:
|
||||
image: aliasvault-smtp
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
7
docker-compose.letsencrypt.yml
Normal file
7
docker-compose.letsencrypt.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
volumes:
|
||||
- ./certificates/letsencrypt:/etc/letsencrypt:rw
|
||||
- ./certificates/letsencrypt/www:/var/www/certbot:rw
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
@@ -1,35 +1,68 @@
|
||||
services:
|
||||
wasm:
|
||||
image: aliasvault
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault.WebApp/Dockerfile
|
||||
reverse-proxy:
|
||||
image: ghcr.io/lanedirt/aliasvault-reverse-proxy:latest
|
||||
ports:
|
||||
- "80:8080"
|
||||
restart: always
|
||||
environment:
|
||||
- API_URL=http://localhost:81
|
||||
|
||||
server:
|
||||
image: aliasvault-server
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault/Dockerfile
|
||||
ports:
|
||||
- "82:8082"
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./database:/database
|
||||
- ./certificates/ssl:/etc/nginx/ssl:rw
|
||||
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
|
||||
- ./certificates/letsencrypt/www:/var/www/certbot:rw
|
||||
depends_on:
|
||||
- admin
|
||||
- client
|
||||
- api
|
||||
- smtp
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
client:
|
||||
image: ghcr.io/lanedirt/aliasvault-client:latest
|
||||
volumes:
|
||||
- ./logs/msbuild:/src/msbuild-logs:rw
|
||||
expose:
|
||||
- "3000"
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
api:
|
||||
image: aliasvault-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault.Api/Dockerfile
|
||||
ports:
|
||||
- "81:8081"
|
||||
image: ghcr.io/lanedirt/aliasvault-api:latest
|
||||
expose:
|
||||
- "3001"
|
||||
volumes:
|
||||
- ./database:/database
|
||||
- ./database:/database:rw
|
||||
- ./certificates/app:/certificates/app:rw
|
||||
- ./logs:/logs:rw
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
admin:
|
||||
image: ghcr.io/lanedirt/aliasvault-admin:latest
|
||||
expose:
|
||||
- "3002"
|
||||
volumes:
|
||||
- ./database:/database:rw
|
||||
- ./certificates/app:/certificates/app:rw
|
||||
- ./logs:/logs:rw
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
smtp:
|
||||
image: ghcr.io/lanedirt/aliasvault-smtp:latest
|
||||
ports:
|
||||
- "25:25"
|
||||
- "587:587"
|
||||
volumes:
|
||||
- ./database:/database:rw
|
||||
- ./logs:/logs:rw
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
aliasvault:
|
||||
name: aliasvault_default
|
||||
|
||||
5
docs/README.md
Normal file
5
docs/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Documentation
|
||||
This is the documentation for the AliasVault project.
|
||||
|
||||
## Description
|
||||
TODO: Work in progress.
|
||||
12
docs/client/enable-webauthn-pfr-chrome.md
Normal file
12
docs/client/enable-webauthn-pfr-chrome.md
Normal file
@@ -0,0 +1,12 @@
|
||||
The webauthn implementation in order to quick unlock the vault requires the use of a FIDO2 authenticator.
|
||||
|
||||
This can be either the built-in browser authenticator or an external authenticator like a Yubikey.
|
||||
|
||||
At the time of writing (2024-10-04), only some browsers support the required PRF extension. In order to make it work in Chrome, you need to enable the PRF extension in the browser settings.
|
||||
|
||||
## Chrome
|
||||
|
||||
1. Open the Chrome browser and navigate to `chrome://flags/#enable-experimental-web-platform-features`.
|
||||
2. Enable the `Experimental Web Platform features` flag.
|
||||
3. Restart the browser.
|
||||
4. Now it should be possible to use the built-in chrome password manager to unlock the vault.
|
||||
18
docs/dev/configure-sqlite-wasm.md
Normal file
18
docs/dev/configure-sqlite-wasm.md
Normal file
@@ -0,0 +1,18 @@
|
||||
To configure SQLite for use with WebAssembly follow these steps:
|
||||
|
||||
1. Add NuGet package
|
||||
```
|
||||
dotnet add package SQLitePCLRaw.bundle_e_sqlite3
|
||||
```
|
||||
|
||||
2. Modify .csproj and add the following:
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
3. Make sure the "wasm-tools" workload is installed on the local machine in order to build the project:
|
||||
```
|
||||
dotnet workload install wasm-tools
|
||||
```
|
||||
86
docs/dev/run-github-actions-locally.md
Normal file
86
docs/dev/run-github-actions-locally.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Run GitHub Actions Locally
|
||||
|
||||
This guide will help you set up and run GitHub Actions locally on Linux, which can be useful for debugging and testing your workflows without pushing changes to the repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux (Ubuntu or RHEL-based distributions)
|
||||
- [Docker](https://www.docker.com/) installed and running
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Install GitHub CLI
|
||||
|
||||
First, install the GitHub CLI using Homebrew:
|
||||
|
||||
```bash
|
||||
sudo dnf install 'dnf-command(config-manager)'
|
||||
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
||||
sudo dnf install gh --repo gh-cli
|
||||
```
|
||||
|
||||
### 2. Install Nektos/Act
|
||||
|
||||
Next, install the Nektos/Act extension for GitHub CLI:
|
||||
|
||||
```bash
|
||||
gh extension install https://github.com/nektos/gh-act
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
To run GitHub Actions locally, navigate to the root of your Git project and execute:
|
||||
|
||||
```bash
|
||||
act
|
||||
```
|
||||
|
||||
This command will pull the necessary Docker containers and execute the GitHub Actions defined in your repository.
|
||||
|
||||
### Understanding the `-P` Option
|
||||
|
||||
By default, `act` uses a simple Docker container that is small in size. However, official GitHub runners are much larger (10GB or even 100GB+). When certain commands or environments are needed, you should specify the full runner image using the `-P` option.
|
||||
|
||||
The `-P` option allows you to map the platform to a specific Docker image. This is particularly useful when you need to replicate the environment of the official GitHub runners more closely.
|
||||
|
||||
Syntax:
|
||||
```bash
|
||||
act -P ubuntu-latest=catthehacker/ubuntu:full-latest
|
||||
```
|
||||
|
||||
This command tells `act` to use the `catthehacker/ubuntu:full-latest` Docker image for the `ubuntu-latest` platform, which is a more complete representation of the GitHub-hosted runner environment.
|
||||
|
||||
## Debugging E2E Tests for AliasVault
|
||||
|
||||
To run and debug the E2E tests for AliasVault using a more complete runner image, use the following command:
|
||||
|
||||
```bash
|
||||
act -W .github/workflows/dotnet-e2e-tests.yml -P ubuntu-latest=catthehacker/ubuntu:full-latest
|
||||
```
|
||||
|
||||
This command does the following:
|
||||
- `-W .github/workflows/dotnet-e2e-tests.yml`: Specifies the workflow file to run
|
||||
- `-P ubuntu-latest=catthehacker/ubuntu:full-latest`: Uses a more complete Ubuntu image that better replicates the GitHub-hosted runner environment
|
||||
|
||||
Running this command will execute the E2E tests locally, allowing you to debug and test your workflow without pushing changes to the repository.
|
||||
|
||||
```bash
|
||||
docker image prune -a -f && docker system prune -a -f
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Handling Disk Space Errors
|
||||
|
||||
If you encounter disk space errors, you can free up space by pruning Docker images and system resources:
|
||||
|
||||
### Misc
|
||||
|
||||
If you encounter any issues while running GitHub Actions locally, consider the following:
|
||||
|
||||
1. Ensure Docker is running and has sufficient resources allocated.
|
||||
2. Check that your workflow file is correctly formatted and placed in the `.github/workflows/` directory.
|
||||
3. Verify that all required secrets and environment variables are properly set.
|
||||
4. If you're using specific tools or commands that are available in GitHub-hosted runners but not in the default `act` image, make sure to use the `-P` option with an appropriate image as shown in the E2E tests example.
|
||||
|
||||
For more detailed information and advanced usage, refer to the [Nektos/Act GitHub repository](https://github.com/nektos/act).
|
||||
12
docs/dev/upgrade-ef-client-model.md
Normal file
12
docs/dev/upgrade-ef-client-model.md
Normal file
@@ -0,0 +1,12 @@
|
||||
To upgrade the AliasClientDb EF model, follow these steps:
|
||||
|
||||
1. Make changes to the AliasClientDb EF model in the `AliasClientDb` project.
|
||||
2. Create a new migration by running the following command in the `AliasClientDb` project:
|
||||
|
||||
```bash
|
||||
# Important: make sure the migration name is prefixed by the Semver version number of the release.
|
||||
# For example, if the release version is 1.0.0, the migration name should be `1.0.0-<migration-name>`.
|
||||
dotnet ef migrations add "1.0.0-<migration-name>"
|
||||
```
|
||||
4. On the next login of a user, they will be prompted (required) to upgrade their database schema to the latest version.
|
||||
Make sure to manually test this.
|
||||
30
docs/diagrams/README.md
Normal file
30
docs/diagrams/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Diagrams
|
||||
|
||||
This folder contains architecture and flow diagrams for AliasVault in various formats.
|
||||
|
||||
## Draw.io Diagrams (.drawio)
|
||||
Files with `.drawio` extension are created with Draw.io (also known as diagrams.net), an open-source diagramming tool.
|
||||
|
||||
### How to Open/Edit Draw.io Files
|
||||
1. Web Interface (Cloud)
|
||||
- Visit [diagrams.net](https://app.diagrams.net/)
|
||||
- Open source code available at [github.com/jgraph/drawio](https://github.com/jgraph/drawio)
|
||||
|
||||
2. Desktop Applications (Offline)
|
||||
- Available for Windows, macOS, and Linux
|
||||
- Download from [github.com/jgraph/drawio-desktop/releases](https://github.com/jgraph/drawio-desktop/releases)
|
||||
- Open source code available at [github.com/jgraph/drawio-desktop](https://github.com/jgraph/drawio-desktop)
|
||||
|
||||
3. VS Code Extension
|
||||
- Install the [Draw.io Integration](https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio) extension
|
||||
- Edit diagrams directly within VS Code
|
||||
- Source code at [github.com/hediet/vscode-drawio](https://github.com/hediet/vscode-drawio)
|
||||
|
||||
## Mermaid Diagrams (.mmd)
|
||||
Files with `.mmd` extension are [Mermaid](https://mermaid.js.org/) format diagrams. These are text-based diagram definitions that can be rendered by various tools.
|
||||
|
||||
### Editors & Tools for Mermaid
|
||||
- [Mermaid Live Editor](https://github.com/mermaid-js/mermaid-live-editor) - Web-based editor with live preview
|
||||
- [VS Code Mermaid Extension](https://github.com/mermaid-js/vscode-mermaid) - Preview and edit Mermaid diagrams directly in VS Code
|
||||
- [Obsidian Mermaid Plugin](https://github.com/jobindj/obsidian-mermaid) - If you use Obsidian for documentation
|
||||
- [GitLab](https://docs.gitlab.com/ee/user/markdown.html#mermaid) and [GitHub](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/) both render Mermaid diagrams natively in markdown
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1024 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,452 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.7.17 Chrome/128.0.6613.36 Electron/32.0.1 Safari/537.36" version="24.7.17">
|
||||
<diagram name="Page-1" id="ykhTdbPCDOXpVAqZYsCj">
|
||||
<mxGraphModel dx="1775" dy="1249" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-1" value="Legend" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;fontSize=16;align=left;verticalAlign=top;fillColor=#0050ef;strokeColor=#001DBC;fontColor=#ffffff;fontStyle=1;spacingLeft=6;spacing=0;resizable=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="275" y="1058" width="723" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-2" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-1">
|
||||
<mxGeometry y="30" width="723" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-3" value="Cryptographic Operations" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=#432D57;overflow=hidden;fillColor=#76608a;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=12;align=left;fontColor=#ffffff;spacingLeft=10;spacingRight=4;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-2">
|
||||
<mxGeometry width="163" height="30" as="geometry">
|
||||
<mxRectangle width="163" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-203" value="Storage Elements" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=#006EAF;overflow=hidden;fillColor=#1ba1e2;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=12;align=left;fontColor=#ffffff;spacingLeft=10;spacingRight=4;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-2">
|
||||
<mxGeometry x="163" width="120" height="30" as="geometry">
|
||||
<mxRectangle width="120" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-208" value="<span style="color: rgb(0, 0, 0);">Keys and Sensitive Data</span>" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=#B09500;overflow=hidden;fillColor=#e3c800;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=12;align=left;fontColor=#000000;spacingLeft=10;spacingRight=4;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-2">
|
||||
<mxGeometry x="283" width="170" height="30" as="geometry">
|
||||
<mxRectangle width="170" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-213" value="Authentication Steps" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=#2D7600;overflow=hidden;fillColor=#60a917;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=12;align=left;fontColor=#ffffff;spacingLeft=10;spacingRight=4;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-2">
|
||||
<mxGeometry x="453" width="150" height="30" as="geometry">
|
||||
<mxRectangle width="150" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-218" value="Process step" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=#6D1F00;overflow=hidden;fillColor=#a0522d;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=12;align=left;fontColor=#ffffff;spacingLeft=10;spacingRight=4;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-2">
|
||||
<mxGeometry x="603" width="120" height="30" as="geometry">
|
||||
<mxRectangle width="120" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-15" value="Client (WebAssembly)" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="52" width="1340" height="470" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-63" value="Vault operations" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="526.84" y="82" width="160" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-32" value="Server (REST API)" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="652" width="1340" height="390" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-43" value="" style="group;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" connectable="0" parent="1">
|
||||
<mxGeometry x="70" y="682.65" width="410" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-33" value="Authentication flow" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry width="410" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-34" value="SRP server verification" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;strokeColor=#432D57;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="150" y="40" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-35" value="2FA (Optional)" style="text;html=1;align=center;verticalAlign=top;whiteSpace=wrap;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="16" y="120" width="378" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-81" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-43" source="6F2B19X3ZkVbRV3rCgbW-36" target="6F2B19X3ZkVbRV3rCgbW-37">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-36" value="Google Authenticator or compatible" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="34" y="155" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-82" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-43" source="6F2B19X3ZkVbRV3rCgbW-37" target="6F2B19X3ZkVbRV3rCgbW-38">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-37" value="Time-based OTP" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="154" y="155" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-38" value="Verify OTP code" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="274" y="155" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-39" value="Issue JWT token" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-43">
|
||||
<mxGeometry x="150" y="239.35" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-40" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-43" source="6F2B19X3ZkVbRV3rCgbW-34" target="6F2B19X3ZkVbRV3rCgbW-35">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="-256" y="440" as="sourcePoint" />
|
||||
<mxPoint x="-206" y="390" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-41" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-43" source="6F2B19X3ZkVbRV3rCgbW-35" target="6F2B19X3ZkVbRV3rCgbW-39">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="215" y="100" as="sourcePoint" />
|
||||
<mxPoint x="215" y="130" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-44" value="" style="endArrow=classic;html=1;rounded=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-26" target="6F2B19X3ZkVbRV3rCgbW-66">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="369" y="640" as="sourcePoint" />
|
||||
<mxPoint x="419" y="590" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-45" value="SRP handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-44">
|
||||
<mxGeometry x="-0.1654" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="8" y="15" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-27" value="AES256-GCM" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
|
||||
<mxGeometry x="554" y="222" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-26" value="SRP client" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
|
||||
<mxGeometry x="164" y="453" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-60" value="Key derivation and server authentication" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="95" y="82" width="250" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-16" value="Master Password <br>(not persisted)" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="145" y="172" width="150" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-20" value="Argon2Id" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
|
||||
<mxGeometry x="165" y="247" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-22" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-16" target="6F2B19X3ZkVbRV3rCgbW-20">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="405" y="282" as="sourcePoint" />
|
||||
<mxPoint x="455" y="232" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-23" value="Derived Key <br>(stored in app memory)" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="144" y="320" width="150" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-24" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-20" target="6F2B19X3ZkVbRV3rCgbW-23">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="405" y="282" as="sourcePoint" />
|
||||
<mxPoint x="455" y="232" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-28" value="" style="endArrow=classic;html=1;rounded=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;curved=1;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-23" target="6F2B19X3ZkVbRV3rCgbW-26">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="420" y="552" as="sourcePoint" />
|
||||
<mxPoint x="470" y="502" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-30" value="Used for authentication<br>with server" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-28">
|
||||
<mxGeometry x="-0.1756" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="-2" y="24" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-67" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-66" target="6F2B19X3ZkVbRV3rCgbW-34">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-66" value="SRP salt/verifier" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="144" y="559" width="150" height="48" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-72" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-68" target="6F2B19X3ZkVbRV3rCgbW-27">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="609" y="392" />
|
||||
<mxPoint x="609" y="392" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-180" value="Decrypt with<br>derived key" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-72">
|
||||
<mxGeometry x="0.6371" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-68" value="Retrieve encrypted vault" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="1">
|
||||
<mxGeometry x="549" y="330" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-47" value="Server storage" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;container=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="540" y="682.65" width="140" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-70" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-56" target="6F2B19X3ZkVbRV3rCgbW-68">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-181" value="Retrieve vault <br>from server" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-70">
|
||||
<mxGeometry x="0.2454" y="1" relative="1" as="geometry">
|
||||
<mxPoint y="-26" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-56" value="Encrypted Vault(s)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;container=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="556" y="712.65" width="110" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-76" value="Claiming new email address" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="720" y="82" width="250" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-83" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-77" target="6F2B19X3ZkVbRV3rCgbW-79">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-100" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-77" target="6F2B19X3ZkVbRV3rCgbW-78">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-77" value="RSA/OAEP Key Generation" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
|
||||
<mxGeometry x="790" y="122" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-78" value="Public key" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="852" y="202" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-79" value="Private key" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="742" y="202" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-86" value="" style="group;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" connectable="0" parent="1">
|
||||
<mxGeometry x="870" y="687" width="490" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-87" value="Email system" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry width="490" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-113" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-86" source="6F2B19X3ZkVbRV3rCgbW-109" target="6F2B19X3ZkVbRV3rCgbW-112">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-114" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-113">
|
||||
<mxGeometry x="-0.1453" y="2" relative="1" as="geometry">
|
||||
<mxPoint x="2" y="-12" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-116" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-86" source="6F2B19X3ZkVbRV3rCgbW-109" target="6F2B19X3ZkVbRV3rCgbW-115">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-117" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-116">
|
||||
<mxGeometry x="0.3626" y="3" relative="1" as="geometry">
|
||||
<mxPoint x="18" y="-2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-109" value="Valid Email Claim?" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry x="298.780487804878" y="225" width="95.60975609756098" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-112" value="Reject email" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry x="380" y="155.65" width="103.41" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-120" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-86" source="6F2B19X3ZkVbRV3rCgbW-115" target="6F2B19X3ZkVbRV3rCgbW-121">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="89.6341463414634" y="201.30000000000007" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-125" value="Encrypt email contents <br>with symmetric key <div><br/></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-120">
|
||||
<mxGeometry x="-0.4172" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="1" y="-1" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-133" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-86" source="6F2B19X3ZkVbRV3rCgbW-115" target="6F2B19X3ZkVbRV3rCgbW-124">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-115" value="Random generated<br>symmetric key" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry x="239.9982926829268" y="145.65" width="131.46341463414635" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-121" value="AES256-GCM" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry x="95.60975609756098" y="35.650000000000006" width="119.51219512195121" height="35.65" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-137" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="6F2B19X3ZkVbRV3rCgbW-86" source="6F2B19X3ZkVbRV3rCgbW-124" target="6F2B19X3ZkVbRV3rCgbW-121">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-124" value="Encrypt symmetric key with public key" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="6F2B19X3ZkVbRV3rCgbW-86">
|
||||
<mxGeometry x="70.00121951219514" y="105.65" width="131.46341463414635" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-78" target="6F2B19X3ZkVbRV3rCgbW-99">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="892" y="292" />
|
||||
<mxPoint x="750" y="292" />
|
||||
<mxPoint x="750" y="882" />
|
||||
<mxPoint x="666" y="882" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-102" value="Save in server <br>public key store" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-101">
|
||||
<mxGeometry x="0.4464" y="-2" relative="1" as="geometry">
|
||||
<mxPoint x="9" y="-242" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-103" value="Register new email<br>claim" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="1">
|
||||
<mxGeometry x="781" y="332" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-140" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-99" target="6F2B19X3ZkVbRV3rCgbW-124">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-141" value="Retrieve public key<div>associated with email claim</div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-140">
|
||||
<mxGeometry x="0.4692" relative="1" as="geometry">
|
||||
<mxPoint x="17" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-99" value="Public key store" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="556" y="872.65" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-138" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-105" target="6F2B19X3ZkVbRV3rCgbW-109">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-153" value="Retrieve registered email <br>address claims" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-138">
|
||||
<mxGeometry x="0.2388" y="-5" relative="1" as="geometry">
|
||||
<mxPoint x="-10" y="-2" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-105" value="Email claim store" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="555" y="932.65" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-158" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-107" target="6F2B19X3ZkVbRV3rCgbW-109">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-107" value="External email received from internet" style="shape=message;html=1;html=1;outlineConnect=0;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="1186.3899999999999" y="1058" width="60" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-106" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="830" y="372" as="sourcePoint" />
|
||||
<mxPoint x="665" y="942" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="830" y="372" />
|
||||
<mxPoint x="830" y="942" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-152" value="Register email claim<br>on server" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-106">
|
||||
<mxGeometry x="-0.3735" relative="1" as="geometry">
|
||||
<mxPoint x="11" y="-153" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-123" value="Encrypted email(s)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="556" y="812.65" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-134" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-121" target="6F2B19X3ZkVbRV3rCgbW-123">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="1025" y="703" />
|
||||
<mxPoint x="850" y="703" />
|
||||
<mxPoint x="850" y="843" />
|
||||
<mxPoint x="666" y="843" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-135" value="Store encrypted email" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-134">
|
||||
<mxGeometry x="0.6857" relative="1" as="geometry">
|
||||
<mxPoint x="-1" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-136" value="Email retrieval and decryption" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;" vertex="1" parent="1">
|
||||
<mxGeometry x="996.3900000000001" y="82" width="250" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-145" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-143" target="6F2B19X3ZkVbRV3rCgbW-144">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-146" value="Decrypt email using private key<br>&nbsp;stored in vault" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-145">
|
||||
<mxGeometry x="0.1105" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-143" value="Retrieve encrypted email" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="1">
|
||||
<mxGeometry x="1061.39" y="122" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-148" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-144" target="6F2B19X3ZkVbRV3rCgbW-162">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1121.39" y="322" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-144" value="AES256-GCM" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#76608a;fontColor=#ffffff;strokeColor=#432D57;" vertex="1" parent="1">
|
||||
<mxGeometry x="1066.39" y="232" width="110" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-150" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-123">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1060" y="142" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="672" y="823" />
|
||||
<mxPoint x="920" y="822" />
|
||||
<mxPoint x="920" y="482" />
|
||||
<mxPoint x="1040" y="482" />
|
||||
<mxPoint x="1040" y="142" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-151" value="Retrieve encrypted email" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-150">
|
||||
<mxGeometry x="-0.6389" y="-1" relative="1" as="geometry">
|
||||
<mxPoint x="-117" y="-1" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-159" value="" style="shape=image;aspect=fixed;image=data:image/svg+xml,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA1MDAgNTAwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MDAgNTAwIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJtNDU5Ljg3IDI5NC45NWMwLjAxNjIwNSA1LjQwMDUgMC4wMzI0MSAxMC44MDEtMC4zNTAyMiAxNi44NzMtMS4xMTEgNi4zMzkyLTEuMTk0MSAxMi4xNzMtMi42MzUxIDE3LjY0OS0xMC45MjIgNDEuNTA4LTM2LjczMSA2OS40ODEtNzcuMzUxIDgzLjQwOC03LjIxNTcgMi40NzM5LTE0Ljk3MiAzLjM3MDItMjIuNDc5IDQuOTk1LTIzLjYyOSAwLjA0MjIwNS00Ny4yNTcgMC4xMTQ1My03MC44ODYgMC4xMjAyNy00Ni43NjIgMC4wMTEzMjItOTMuNTIzLTAuMDE0MTYtMTQwLjk1LTAuNDM0MTEtOC41OS0yLjAwMjQtMTYuNzY2LTIuODM1Mi0yNC4zOTgtNS4zMzI2LTIxLjU5NS03LjA2NjYtMzkuNTIzLTE5LjY1Ni01My43MDgtMzcuNTUyLTEwLjIyNy0xMi45MDMtMTcuNTc5LTI3LjE3LTIxLjI4LTQzLjIyMS0xLjQ3NS02LjM5NjctMi40NzExLTEyLjkwNC0zLjY4NTItMTkuMzYxLTAuMDUxODQ5LTUuNzQ3LTAuMTAzNy0xMS40OTQgMC4yNjkxNS0xNy44ODYgNC4xNTktNDIuOTczIDI3LjY4LTcxLjYzOCA2My41NjItOTIuMTUzIDAtMC43MDc2MS0wLjAwMTk2MS0xLjY5ODggMy4xMmUtNCAtMi42OSAwLjAyMjQ4NC05LjgyOTMtMS4zMDcxLTE5Ljg5NCAwLjM1NjY0LTI5LjQzOCAzLjIzOTEtMTguNTc5IDExLjA4LTM1LjI3MiAyMy43NjMtNDkuNzczIDEyLjA5OC0xMy44MzIgMjYuNDU3LTIzLjk4OSA0My42MDktMzAuMDI5IDcuODEzLTIuNzUxMiAxNi4xNC00LjA0MTcgMjQuMjM0LTUuOTk0OCA3LjM5Mi0wLjAyNTczNCAxNC43ODQtMC4wNTE0NiAyMi44MzUgMC4zMjI1MyA0LjE5NTkgMC45NTM5MiA3Ljc5NDYgMS4yNTM4IDExLjI1OCAyLjEwNTMgMTcuMTYgNC4yMTkyIDMyLjI4NyAxMi4xNzYgNDUuNDY5IDI0LjEwNCAyLjI1NTggMi4wNDExIDQuMzcyIDYuNjI0MSA5LjYyMSAzLjg2OCAxNi44MzktOC44NDE5IDM0LjcxOC0xMS41OTcgNTMuNjAzLTguNTk0IDE2Ljc5MSAyLjY2OTkgMzEuNjAyIDkuNDMwOCA0NC4yMzYgMjAuNjM2IDExLjUzMSAxMC4yMjcgMTkuODQgMjIuODQxIDI1LjM5MyAzNy4yMzYgNi4zNDM2IDE2LjQ0NSAxMC4zODkgMzMuMTYzIDYuMDc5OCA0OS4zODkgNy45NTg3IDguOTMyMSAxNS44MDcgMTYuNzA0IDIyLjQyMSAyNS40MTQgOS4xNjIgMTIuMDY1IDE1LjMzIDI1Ljc0NiAxOC4xNDQgNDAuNzc2IDAuOTcwNDYgNS4xODQ4IDEuOTExMSAxMC4zNzUgMi44NjU0IDE1LjU2M20tNzEuNTk3IDcxLjAxMmM1LjU2MTUtNS4yMjg0IDEyLjAwMi05Ljc5ODYgMTYuNTA4LTE1LjgxNyAxMC40NzQtMTMuOTkyIDE0LjMzMy0yOS45MTYgMTEuMjg4LTQ3LjQ0Ni0yLjI0OTYtMTIuOTUtOC4xOTczLTI0LjA3Ni0xNy4yNDMtMzMuMDYzLTEyLjc0Ni0xMi42NjMtMjguODY1LTE4LjYxNC00Ni43ODYtMTguNTY5LTY5LjkxMiAwLjE3NzEyLTEzOS44MiAwLjU2ODMxLTIwOS43NCAwLjk2MTc2LTE1LjkyMiAwLjA4OTU5OS0yOS4xNjggNy40MjA5LTM5LjY4NSAxOC4yOTYtMTQuNDUgMTQuOTQ0LTIwLjQwOCAzMy4zNDMtMTYuNjU1IDU0LjM2OCAyLjI3NjMgMTIuNzU0IDguMjE2NyAyMy43NDggMTcuMTU4IDMyLjY2IDEzLjI5OSAxMy4yNTUgMzAuMDk3IDE4LjY1MyA0OC43MjggMTguNjUxIDU5LjMyMS0wLjAwNTE4OCAxMTguNjQgMC4wNDIzNTggMTc3Ljk2LTAuMDQ2NjAxIDkuNTkxMi0wLjAxNDM3NCAxOS4xODEtMC44NjU4OCAyOC43NzMtMC44ODg1NSAxMC42NDktMC4wMjUxNDYgMTkuOTc4LTMuODI1IDI5LjY4Ny05LjEwNzR6IiBmaWxsPSIjRUVDMTcwIi8+CjxwYXRoIGQ9Im0xNjIuNzcgMjkzYzE1LjY1NCA0LjM4ODMgMjAuNjI3IDIyLjk2NyAxMC4zMDQgMzQuOTgtNS4zMTA0IDYuMTc5NS0xNC44MTcgOC4zMjA4LTI0LjI3OCA1LjA0NzItNy4wNzIzLTIuNDQ3MS0xMi4zMzItMTAuMzYyLTEyLjg3Ni0xNy45MzMtMS4wNDUxLTE0LjU0MiAxMS4wODktMjMuMTc2IDIxLjcwNS0yMy4wNDYgMS41Nzk0IDAuMDE5Mjg3IDMuMTUxNyAwLjYxNTY2IDUuMTQ2MSAwLjk1MTg0eiIgZmlsbD0iI0VFQzE3MCIvPgo8cGF0aCBkPSJtMjI3LjE4IDI5My42NGM3Ljg0OTkgMi4zOTczIDExLjkzOCA4LjIxNDMgMTMuNTI0IDE1LjA3NyAxLjg1OTEgOC4wNDM5LTAuNDQ4MTcgMTUuNzA2LTcuMTU4OCAyMS4xMjEtNi43NjMzIDUuNDU3Mi0xNC40MTcgNi44Nzk0LTIyLjU3OCAzLjE0ODMtOC4yOTcyLTMuNzkzMy0xMi44MzYtMTAuODQ5LTEyLjczNi0xOS40MzggMC4xNjg3LTE0LjQ5NyAxNC4xMy0yNS4zNjggMjguOTQ4LTE5LjkwOHoiIGZpbGw9IiNFRUMxNzAiLz4KPHBhdGggZD0ibTI2MS41NyAzMTkuMDdjLTIuNDk1LTE0LjQxOCA0LjY4NTMtMjIuNjAzIDE0LjU5Ni0yNi4xMDggOS44OTQ1LTMuNDk5NSAyMy4xODEgMy40MzAzIDI2LjI2NyAxMy43NzkgNC42NTA0IDE1LjU5MS03LjE2NTEgMjkuMDY0LTIxLjY2NSAyOC4xNjEtOC41MjU0LTAuNTMwODgtMTcuMjAyLTYuNTA5NC0xOS4xOTgtMTUuODMxeiIgZmlsbD0iI0VFQzE3MCIvPgo8cGF0aCBkPSJtMzM2LjkxIDMzMy40MWMtOS4wMTc1LTQuMjQ5MS0xNS4zMzctMTQuMzQ5LTEzLjgyOS0yMS42ODIgMy4wODI1LTE0Ljk4OSAxMy4zNDEtMjAuMzA0IDIzLjAxOC0xOS41ODUgMTAuNjUzIDAuNzkxNDEgMTcuOTMgNy40MDcgMTkuNzY1IDE3LjU0NyAxLjk1ODggMTAuODI0LTQuMTE3MSAxOS45MzktMTMuNDk0IDIzLjcwMy01LjI3MiAyLjExNjItMTAuMDkxIDEuNTA4Ni0xNS40NiAwLjAxNzg4M3oiIGZpbGw9IiNFRUMxNzAiLz4KPC9zdmc+Cg==;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="1060" width="58" height="58" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-160" value="AliasVault" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=24;fontStyle=1" vertex="1" parent="1">
|
||||
<mxGeometry x="100" y="1064" width="130" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-161" value="Security architecture 0.6.0" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
|
||||
<mxGeometry x="98" y="1088" width="160" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-162" value="Decrypted email" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="1056.39" y="320" width="130" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-58" value="JWT token for <br>authenticating<br>with REST API" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#e3c800;fontColor=#000000;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="364" y="559" width="155" height="48" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-165" value="Client local storage" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;container=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="370" y="82" width="140" height="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-166" value="Decrypted vault<br>(app memory)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;container=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="380" y="112" width="110" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-167" value="JWT Token<br>(browser local storage)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="383" y="312" width="116" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-169" value="Derived Key<br>(app<span style="background-color: initial;">&nbsp;memory)</span>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="383" y="242" width="116" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-84" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-79" target="6F2B19X3ZkVbRV3rCgbW-166">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="630" y="222" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="752" y="152" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-85" value="Stored in vault" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-84">
|
||||
<mxGeometry x="0.3345" y="3" relative="1" as="geometry">
|
||||
<mxPoint x="7" y="-3" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-170" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="6F2B19X3ZkVbRV3rCgbW-169">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="280" y="342" as="sourcePoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="320" y="342" />
|
||||
<mxPoint x="320" y="267" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-173" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-58" target="6F2B19X3ZkVbRV3rCgbW-167">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-174" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-39" target="6F2B19X3ZkVbRV3rCgbW-58">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="500" y="942" />
|
||||
<mxPoint x="500" y="632" />
|
||||
<mxPoint x="442" y="632" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-176" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-175" target="6F2B19X3ZkVbRV3rCgbW-16">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-175" value="User login" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#a0522d;fontColor=#ffffff;strokeColor=#6D1F00;" vertex="1" parent="1">
|
||||
<mxGeometry x="162" y="112" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-178" value="<span style="">JWT token protected API endpoints</span>" style="shape=corner;whiteSpace=wrap;html=1;fontColor=default;fillColor=#e3c800;strokeColor=#B09500;" vertex="1" parent="1">
|
||||
<mxGeometry x="567" y="552" width="803" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-179" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="6F2B19X3ZkVbRV3rCgbW-27" target="6F2B19X3ZkVbRV3rCgbW-166">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="609" y="212" />
|
||||
<mxPoint x="435" y="212" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="6F2B19X3ZkVbRV3rCgbW-182" value="Store vault <br>in local WASM memory" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6F2B19X3ZkVbRV3rCgbW-179">
|
||||
<mxGeometry x="-0.4412" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="28" y="-21" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
File diff suppressed because one or more lines are too long
118
docs/diagrams/security-architecture/security-architecture.mmd
Normal file
118
docs/diagrams/security-architecture/security-architecture.mmd
Normal file
@@ -0,0 +1,118 @@
|
||||
%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
|
||||
graph TB
|
||||
%% Legend
|
||||
subgraph Legend["Legend"]
|
||||
L_CRYPTO["Cryptographic Operations"]
|
||||
L_STORAGE["Storage Elements"]
|
||||
L_KEY["Keys and Sensitive Data"]
|
||||
L_PROCESS["Process Steps"]
|
||||
L_AUTH["Authentication Steps"]
|
||||
L_FLOW["Email Processing Flow"]
|
||||
end
|
||||
|
||||
subgraph Client["Client (Local Only Operations)"]
|
||||
direction TB
|
||||
MP[/"Master Password\n(never leaves client)"/]
|
||||
|
||||
subgraph KD["1. Key Derivation"]
|
||||
A2["Argon2id"]
|
||||
DK[/"Derived Key"/]
|
||||
MP --> A2
|
||||
A2 --> DK
|
||||
DK --> |"used for vault\nencryption/decryption"| AES
|
||||
DK --> |"used for authentication"| SRP_C
|
||||
end
|
||||
|
||||
subgraph VE["3. Vault Operations"]
|
||||
AES["AES256-GCM"]
|
||||
VAULT["Encrypted Vault Contents"]
|
||||
AES --> |"encrypt/decrypt"| VAULT
|
||||
end
|
||||
|
||||
subgraph KP["4. Email Key Management"]
|
||||
RSA["RSA/OAEP Key Generation"]
|
||||
PRK[/"Private Key\n(stored in vault)"/]
|
||||
PBK[/"Public Key"/]
|
||||
RSA --> |"generates pair"| PRK
|
||||
RSA --> |"generates pair"| PBK
|
||||
end
|
||||
|
||||
subgraph ED["5. Email Decryption"]
|
||||
PRK --> |"decrypt symmetric key"| SK[/"Symmetric Key\n(AES256)"/]
|
||||
SK --> |"decrypt email"| DC["Decrypted Email"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Server["Server"]
|
||||
direction TB
|
||||
|
||||
subgraph AUTH["Authentication Flow"]
|
||||
SRP_S["SRP Verification"]
|
||||
subgraph FA["2FA (Optional)"]
|
||||
TOTP["Time-based OTP"]
|
||||
GA["Google Authenticator\nor Compatible App"]
|
||||
VERIFY["Verify OTP Code"]
|
||||
GA --> |"generate code"| TOTP
|
||||
TOTP --> |"user enters"| VERIFY
|
||||
end
|
||||
JWT["Issue JWT Token"]
|
||||
SRP_S --> FA
|
||||
FA --> |"if 2FA enabled"| JWT
|
||||
SRP_S --> |"if 2FA disabled"| JWT
|
||||
end
|
||||
|
||||
subgraph VS["Vault Storage"]
|
||||
EV["Encrypted Vault Data"]
|
||||
end
|
||||
|
||||
subgraph ES["Email System"]
|
||||
EC["Email Claims"]
|
||||
PKS["Public Key Store"]
|
||||
|
||||
subgraph EP["Email Processing"]
|
||||
CHECK{"Valid\nEmail Claim?"}
|
||||
REJECT["Reject Email"]
|
||||
ESK[/"Generate Random\nSymmetric Key"/]
|
||||
EE["Encrypt Email\nContent"]
|
||||
ESP["Encrypt Symmetric Key\nwith Public Key"]
|
||||
EST["Store Encrypted Email\n& Encrypted Sym Key"]
|
||||
|
||||
CHECK --> |"No"| REJECT
|
||||
CHECK --> |"Yes"| ESK
|
||||
ESK --> EE
|
||||
ESK --> ESP
|
||||
EE --> EST
|
||||
ESP --> EST
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
%% Client-Server Interactions
|
||||
SRP_C["SRP Client"] <--> |"SRP Authentication"| SRP_S
|
||||
AES <--> |"encrypted vault transfer"| EV
|
||||
PBK --> |"register"| PKS
|
||||
EST --> |"retrieve encrypted\nemail & sym key"| ED
|
||||
|
||||
%% Styling
|
||||
classDef process fill:#ddd,stroke:#333,stroke-width:2px
|
||||
classDef storage fill:#b7e3fc,stroke:#333,stroke-width:2px
|
||||
classDef key fill:#fef08a,stroke:#333,stroke-width:2px
|
||||
classDef crypto fill:#e9d5ff,stroke:#333,stroke-width:2px
|
||||
classDef auth fill:#86efac,stroke:#333,stroke-width:2px
|
||||
classDef flow fill:#fca5a5,stroke:#333,stroke-width:2px
|
||||
|
||||
%% Apply styles to legend
|
||||
class L_CRYPTO crypto
|
||||
class L_STORAGE storage
|
||||
class L_KEY key
|
||||
class L_PROCESS process
|
||||
class L_AUTH auth
|
||||
class L_FLOW flow
|
||||
|
||||
%% Apply styles to elements
|
||||
class A2,SRP_C,SRP_S,RSA,AES crypto
|
||||
class EV,EST storage
|
||||
class MP,DK,PRK,PBK,SK,ESK key
|
||||
class KD,VE,KP,ED process
|
||||
class SRP_S,FA,JWT,TOTP,VERIFY auth
|
||||
class CHECK,EP flow
|
||||
BIN
docs/img/screenshot.png
Normal file
BIN
docs/img/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
133
docs/install/1-manually-setup-docker.md
Normal file
133
docs/install/1-manually-setup-docker.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Manual Setup Instructions for AliasVault
|
||||
|
||||
This README provides step-by-step instructions for manually setting up AliasVault without using the `install.sh` script. Follow these steps if you prefer to execute all statements yourself.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed on your system
|
||||
- OpenSSL for generating random passwords
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Create required directories**
|
||||
|
||||
Create the following directories in your project root:
|
||||
```bash
|
||||
mkdir -p certificates/ssl certificates/app database logs/msbuild
|
||||
```
|
||||
|
||||
2. **Create .env file**
|
||||
|
||||
Copy the `.env.example` file to create a new `.env` file:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. **Set HOSTNAME**
|
||||
|
||||
Update the .env file with your hostname (default is localhost):
|
||||
```bash
|
||||
HOSTNAME=localhost
|
||||
```
|
||||
|
||||
4. **Generate and set JWT_KEY**
|
||||
|
||||
Generate a random 32-char string for JWT token generation:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Add the generated key to the .env file:
|
||||
```bash
|
||||
JWT_KEY=your_generated_key_here
|
||||
```
|
||||
|
||||
5. **Generate and set DATA_PROTECTION_CERT_PASS**
|
||||
|
||||
Generate a random password for the data protection certificate:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Add it to the .env file:
|
||||
```bash
|
||||
DATA_PROTECTION_CERT_PASS=your_generated_password_here
|
||||
```
|
||||
|
||||
6. **Set PRIVATE_EMAIL_DOMAINS**
|
||||
|
||||
Update the .env file with allowed email domains. Use DISABLED.TLD to disable email support:
|
||||
```bash
|
||||
PRIVATE_EMAIL_DOMAINS=yourdomain.com,anotherdomain.com
|
||||
```
|
||||
Or to disable email:
|
||||
```bash
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
```
|
||||
|
||||
7. **Set SUPPORT_EMAIL (Optional)**
|
||||
|
||||
Add a support email address if desired:
|
||||
```bash
|
||||
SUPPORT_EMAIL=support@yourdomain.com
|
||||
```
|
||||
|
||||
8. **Generate admin password**
|
||||
|
||||
Build the Docker image for password hashing:
|
||||
```bash
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
|
||||
```
|
||||
|
||||
Generate the password hash:
|
||||
```bash
|
||||
docker run --rm installcli "your_preferred_admin_password_here"
|
||||
```
|
||||
|
||||
Add the password hash and generation timestamp to the .env file:
|
||||
```bash
|
||||
ADMIN_PASSWORD_HASH=<output_from_previous_command>
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
9. **Build and start Docker containers**
|
||||
|
||||
Build the Docker Compose stack:
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
Start the Docker Compose stack:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
10. **Access AliasVault**
|
||||
|
||||
AliasVault should now be running. You can access it at:
|
||||
|
||||
- Admin Panel: https://localhost/admin
|
||||
- Username: admin
|
||||
- Password: [Use the password you set in step 8]
|
||||
|
||||
- Client Website: https://localhost/
|
||||
- Create your own account from here
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Make sure to save the admin password you used in step 8 in a secure location.
|
||||
- If you need to reset the admin password in the future, repeat step 8 and restart the Docker containers.
|
||||
- Always keep your .env file secure and do not share it, as it contains sensitive information.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues during the setup:
|
||||
|
||||
1. Check the Docker logs:
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
2. Ensure all required ports (80 and 443) are available and not being used by other services.
|
||||
3. Verify that all environment variables in the .env file are set correctly.
|
||||
|
||||
For further assistance, please refer to the project documentation or seek support through the appropriate channels.
|
||||
75
docs/security-architecture.md
Normal file
75
docs/security-architecture.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Security Architecture
|
||||
|
||||
AliasVault implements a zero-knowledge architecture where sensitive user data and passwords never leave the client device in unencrypted form. Below is a detailed explanation of how the system secures user data and communications.
|
||||
|
||||
## Diagram
|
||||
The security architecture diagram below illustrates all encryption and authentication processes used in AliasVault to secure user data and communications.
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="diagrams/security-architecture/aliasvault-security-architecture-dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="diagrams/security-architecture/aliasvault-security-architecture-light.svg">
|
||||
<img alt="AliasVault Security Architecture Diagram" src="diagrams/security-architecture/aliasvault-security-architecture-light.svg">
|
||||
</picture>
|
||||
|
||||
You can also view the diagram in a browser-friendly HTML format: [AliasVault Security Architecture](diagrams/security-architecture/aliasvault-security-architecture.html)
|
||||
|
||||
## Key Components and Process Flow
|
||||
|
||||
### 1. Key Derivation
|
||||
- When a user enters their master password, it remains strictly on the client device
|
||||
- The master password is processed through Argon2id (a memory-hard key derivation function) locally
|
||||
- The derived key serves two purposes:
|
||||
- Authentication with the server through the SRP protocol
|
||||
- Local encryption/decryption of vault contents using AES-256-GCM
|
||||
|
||||
### 2. Authentication Process
|
||||
1. SRP (Secure Remote Password) Authentication
|
||||
- Enables secure password-based authentication without transmitting the password
|
||||
- Client and server perform a cryptographic handshake to verify identity
|
||||
|
||||
2. Two-Factor Authentication (Optional)
|
||||
- If enabled, requires an additional verification step after successful SRP authentication
|
||||
- Uses Time-based One-Time Password (TOTP) protocol
|
||||
- Compatible with standard authenticator apps (e.g., Google Authenticator)
|
||||
- Server only issues the final JWT access token after successful 2FA verification
|
||||
|
||||
### 3. Vault Operations
|
||||
- All vault contents are encrypted/decrypted locally using AES-256-GCM
|
||||
- The encryption key is derived from the user's master password
|
||||
- Only encrypted data is ever transmitted to or stored on the server
|
||||
- The server never has access to the unencrypted vault contents
|
||||
|
||||
### 4. Email System Security
|
||||
|
||||
#### Key Generation and Storage
|
||||
1. RSA key pair is generated locally on the client
|
||||
2. Private key is stored in the encrypted vault
|
||||
3. Public key is sent to the server and associated with email claim(s)
|
||||
|
||||
#### Email Reception Process
|
||||
1. When an email is received, the server:
|
||||
- Verifies if the recipient has a valid email claim
|
||||
- If no valid claim exists, the email is rejected
|
||||
- If valid, generates a random 256-bit symmetric key
|
||||
- Encrypts the email content using this symmetric key
|
||||
- Encrypts the symmetric key using the recipient's public key
|
||||
- Stores both the encrypted email and encrypted symmetric key
|
||||
|
||||
#### Email Retrieval Process
|
||||
1. Client retrieves encrypted email and encrypted symmetric key from server
|
||||
2. Client uses private key from vault to decrypt the symmetric key
|
||||
3. Client uses decrypted symmetric key to decrypt the email contents
|
||||
4. All decryption occurs locally on the client device
|
||||
|
||||
> Note: The use of a symmetric key for email content encryption and asymmetric encryption for the symmetric key (hybrid encryption) is implemented due to RSA's limitations on encryption string length and for better performance.
|
||||
|
||||
## Security Benefits
|
||||
- Zero-knowledge architecture ensures user data privacy
|
||||
- Master password never leaves the client device
|
||||
- All sensitive operations (key derivation, encryption/decryption) happen locally
|
||||
- Server stores only encrypted data
|
||||
- Multi-layer encryption for emails provides secure communication
|
||||
- Optional 2FA adds an additional security layer
|
||||
- Use of established cryptographic standards (Argon2id, AES-256-GCM, RSA/OAEP)
|
||||
|
||||
This security architecture ensures that even if the server is compromised, user data remains secure as all sensitive operations and keys remain strictly on the client side.
|
||||
33
entrypoint.sh
Normal file
33
entrypoint.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create SSL directory if it doesn't exist
|
||||
mkdir -p /etc/nginx/ssl
|
||||
|
||||
# Generate self-signed SSL certificate if not exists
|
||||
if [ ! -f /etc/nginx/ssl/cert.pem ] || [ ! -f /etc/nginx/ssl/key.pem ]; then
|
||||
echo "Generating new SSL certificate..."
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/nginx/ssl/key.pem \
|
||||
-out /etc/nginx/ssl/cert.pem \
|
||||
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
|
||||
|
||||
# Set proper permissions
|
||||
chmod 644 /etc/nginx/ssl/cert.pem
|
||||
chmod 600 /etc/nginx/ssl/key.pem
|
||||
fi
|
||||
|
||||
# Create the appropriate SSL configuration based on LETSENCRYPT_ENABLED
|
||||
if [ "${LETSENCRYPT_ENABLED}" = "true" ]; then
|
||||
cat > /etc/nginx/ssl.conf << EOF
|
||||
ssl_certificate /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/privkey.pem;
|
||||
EOF
|
||||
else
|
||||
cat > /etc/nginx/ssl.conf << EOF
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Start nginx
|
||||
nginx -g "daemon off;"
|
||||
78
init.sh
78
init.sh
@@ -1,78 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Define colors for CLI output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Define the path to the .env and .env.example files
|
||||
ENV_FILE=".env"
|
||||
ENV_EXAMPLE_FILE=".env.example"
|
||||
|
||||
# Function to generate a new 32-character JWT key
|
||||
generate_jwt_key() {
|
||||
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 | head -c 32
|
||||
}
|
||||
|
||||
# Function to create .env file from .env.example if it doesn't exist
|
||||
create_env_file() {
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf "${GREEN}> .env file created from .env.example.${NC}\n"
|
||||
else
|
||||
touch "$ENV_FILE"
|
||||
printf "${YELLOW}> .env file created as empty because .env.example was not found.${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf "${CYAN}> .env file already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check and populate the .env file with JWT_KEY
|
||||
populate_jwt_key() {
|
||||
if ! grep -q "^JWT_KEY=" "$ENV_FILE" || [ -z "$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "${YELLOW}JWT_KEY not found or empty in $ENV_FILE. Generating a new JWT key...${NC}\n"
|
||||
JWT_KEY=$(generate_jwt_key)
|
||||
if grep -q "^JWT_KEY=" "$ENV_FILE"; then
|
||||
awk -v key="$JWT_KEY" '/^JWT_KEY=/ {$0="JWT_KEY="key} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
printf "JWT_KEY=${JWT_KEY}" >> "$ENV_FILE\n"
|
||||
fi
|
||||
printf "${GREEN}> JWT_KEY has been added to $ENV_FILE.${NC}\n"
|
||||
else
|
||||
printf "${CYAN}> JWT_KEY already exists and has a value in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print the CLI logo
|
||||
print_logo() {
|
||||
printf "${MAGENTA}\n"
|
||||
printf "=========================================================\n"
|
||||
printf " _ _ __ __ _ _ \n"
|
||||
printf " /\ | (_) \ \ / / | | | \n"
|
||||
printf " / \ | |_ __ _ __\ \ / /_ _ _ _| | |_\n"
|
||||
printf " / /\ \ | | |/ _ / __\ \/ / _ | | | | | __|\n"
|
||||
printf " / ____ \| | | (_| \__ \\ / (_| | |_| | | |_ \n"
|
||||
printf " /_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf "\n"
|
||||
printf "=========================================================\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
# Run the functions and print status
|
||||
print_logo
|
||||
printf "${BLUE}Initializing AliasVault...${NC}\n"
|
||||
create_env_file
|
||||
populate_jwt_key
|
||||
printf "${BLUE}Initialization complete.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To build the images and start the containers, run the following command:\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}$ docker compose up -d --build --force-recreate${NC}\n"
|
||||
printf "\n"
|
||||
printf "\n"
|
||||
808
install.sh
Executable file
808
install.sh
Executable file
@@ -0,0 +1,808 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="lanedirt"
|
||||
REPO_NAME="AliasVault"
|
||||
REPO_BRANCH="main"
|
||||
GITHUB_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}"
|
||||
GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]')/$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
# Required files and directories
|
||||
REQUIRED_DIRS=(
|
||||
"certificates/ssl"
|
||||
"certificates/app"
|
||||
"certificates/letsencrypt"
|
||||
"certificates/letsencrypt/www"
|
||||
"database"
|
||||
"logs"
|
||||
"logs/msbuild"
|
||||
)
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# File paths
|
||||
ENV_FILE=".env"
|
||||
ENV_EXAMPLE_FILE=".env.example"
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
print_logo
|
||||
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
|
||||
printf "\n"
|
||||
printf "Commands:\n"
|
||||
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (default)\n"
|
||||
printf " build Build AliasVault from source (takes longer and requires sufficient specs)\n"
|
||||
printf " reset-password Reset admin password\n"
|
||||
printf " uninstall Uninstall AliasVault\n"
|
||||
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
|
||||
|
||||
printf "\n"
|
||||
printf "Options:\n"
|
||||
printf " --verbose Show detailed output\n"
|
||||
printf " -y, --yes Automatic yes to prompts (for uninstall)\n"
|
||||
printf " --help Show this help message\n"
|
||||
}
|
||||
|
||||
# Function to parse command line arguments
|
||||
parse_args() {
|
||||
COMMAND="" # Remove default command
|
||||
VERBOSE=false
|
||||
FORCE_YES=false
|
||||
|
||||
# Show usage if no arguments provided
|
||||
if [ $# -eq 0 ]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
install|i)
|
||||
COMMAND="install"
|
||||
shift
|
||||
;;
|
||||
build|b)
|
||||
COMMAND="build"
|
||||
shift
|
||||
;;
|
||||
uninstall|u)
|
||||
COMMAND="uninstall"
|
||||
shift
|
||||
;;
|
||||
reset-password|reset-admin-password|rp)
|
||||
COMMAND="reset-password"
|
||||
shift
|
||||
;;
|
||||
configure-ssl|ssl)
|
||||
COMMAND="configure-ssl"
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-y|--yes)
|
||||
FORCE_YES=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# Check if command is empty (should not happen with updated parse_args)
|
||||
if [ -z "$COMMAND" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_logo
|
||||
case $COMMAND in
|
||||
"install")
|
||||
handle_install
|
||||
;;
|
||||
"build")
|
||||
handle_build
|
||||
;;
|
||||
"uninstall")
|
||||
handle_uninstall
|
||||
;;
|
||||
"reset-password")
|
||||
generate_admin_password
|
||||
if [ $? -eq 0 ]; then
|
||||
recreate_docker_containers
|
||||
print_password_reset_message
|
||||
fi
|
||||
;;
|
||||
"configure-ssl")
|
||||
handle_ssl_configuration
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to create required directories
|
||||
create_directories() {
|
||||
printf "${CYAN}> Checking workspace...${NC}\n"
|
||||
|
||||
local dirs_needed=false
|
||||
for dir in "${REQUIRED_DIRS[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
if [ "$dirs_needed" = false ]; then
|
||||
printf " ${CYAN}> Creating required directories...${NC}\n"
|
||||
dirs_needed=true
|
||||
fi
|
||||
mkdir -p "$dir"
|
||||
chmod -R 755 "$dir"
|
||||
if [ $? -ne 0 ]; then
|
||||
printf " ${RED}> Failed to create directory: $dir${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$dirs_needed" = true ]; then
|
||||
printf " ${GREEN}> Directories created successfully.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> All required directories already exist.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to initialize workspace
|
||||
initialize_workspace() {
|
||||
create_directories
|
||||
handle_docker_compose
|
||||
}
|
||||
|
||||
# Function to handle docker-compose.yml
|
||||
handle_docker_compose() {
|
||||
printf "${CYAN}> Checking docker-compose.yml...${NC}\n"
|
||||
|
||||
if [ -f "docker-compose.yml" ]; then
|
||||
printf " ${GREEN}> docker-compose.yml already exists.${NC}\n"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf " ${CYAN}> Downloading docker-compose.yml...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL}/docker-compose.yml" -o "docker-compose.yml" > /dev/null 2>&1; then
|
||||
printf "\n ${GREEN}> docker-compose.yml downloaded successfully.${NC}\n"
|
||||
return 0
|
||||
else
|
||||
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from https://github.com/${REPO_OWNER}/${REPO_NAME}/blob/main/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print the logo
|
||||
print_logo() {
|
||||
printf "${MAGENTA}"
|
||||
printf " _ _ _ __ __ _ _ \n"
|
||||
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n"
|
||||
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n"
|
||||
printf " / ___ \| | | (_| \__ \ \ / (_| | |_| | | |_ \n"
|
||||
printf "/_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
# Function to create .env file
|
||||
create_env_file() {
|
||||
printf "${CYAN}> Checking .env file...${NC}\n"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf " ${GREEN}> New.env file created from .env.example.${NC}\n"
|
||||
else
|
||||
touch "$ENV_FILE"
|
||||
printf " ${YELLOW}> New blank .env file created.${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf " ${GREEN}> .env file already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Environment setup functions
|
||||
populate_hostname() {
|
||||
printf "${CYAN}> Checking HOSTNAME...${NC}\n"
|
||||
if ! grep -q "^HOSTNAME=" "$ENV_FILE" || [ -z "$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
DEFAULT_HOSTNAME="localhost"
|
||||
read -p "Enter the hostname where AliasVault will be hosted (press Enter for default: $DEFAULT_HOSTNAME): " USER_HOSTNAME
|
||||
HOSTNAME=${USER_HOSTNAME:-$DEFAULT_HOSTNAME}
|
||||
update_env_var "HOSTNAME" "$HOSTNAME"
|
||||
else
|
||||
HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
printf " ${GREEN}> HOSTNAME already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
populate_jwt_key() {
|
||||
printf "${CYAN}> Checking JWT_KEY...${NC}\n"
|
||||
if ! grep -q "^JWT_KEY=" "$ENV_FILE" || [ -z "$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
JWT_KEY=$(openssl rand -base64 32)
|
||||
update_env_var "JWT_KEY" "$JWT_KEY"
|
||||
else
|
||||
printf " ${GREEN}> JWT_KEY already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
populate_data_protection_cert_pass() {
|
||||
printf "${CYAN}> Checking DATA_PROTECTION_CERT_PASS...${NC}\n"
|
||||
if ! grep -q "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" || [ -z "$(grep "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
CERT_PASS=$(openssl rand -base64 32)
|
||||
update_env_var "DATA_PROTECTION_CERT_PASS" "$CERT_PASS"
|
||||
else
|
||||
printf " ${GREEN}> DATA_PROTECTION_CERT_PASS already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
set_private_email_domains() {
|
||||
printf "${CYAN}> Checking PRIVATE_EMAIL_DOMAINS...${NC}\n"
|
||||
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "Please enter the domains that should be allowed to receive email, separated by commas (press Enter to disable email support): "
|
||||
read -r private_email_domains
|
||||
|
||||
private_email_domains=${private_email_domains:-"DISABLED.TLD"}
|
||||
update_env_var "PRIVATE_EMAIL_DOMAINS" "$private_email_domains"
|
||||
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${RED}SMTP is disabled.${NC}\n"
|
||||
fi
|
||||
else
|
||||
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC} ${RED}Private email domains are disabled.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC}\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
set_smtp_tls_enabled() {
|
||||
printf "${CYAN}> Checking SMTP_TLS_ENABLED...${NC}\n"
|
||||
if ! grep -q "^SMTP_TLS_ENABLED=" "$ENV_FILE"; then
|
||||
update_env_var "SMTP_TLS_ENABLED" "false"
|
||||
else
|
||||
printf " ${GREEN}> SMTP_TLS_ENABLED already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
set_support_email() {
|
||||
printf "${CYAN}> Checking SUPPORT_EMAIL...${NC}\n"
|
||||
if ! grep -q "^SUPPORT_EMAIL=" "$ENV_FILE"; then
|
||||
read -p "Enter support email address (optional, press Enter to skip): " SUPPORT_EMAIL
|
||||
update_env_var "SUPPORT_EMAIL" "$SUPPORT_EMAIL"
|
||||
else
|
||||
printf " ${GREEN}> SUPPORT_EMAIL already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to generate admin password
|
||||
generate_admin_password() {
|
||||
printf "${CYAN}> Generating admin password...${NC}\n"
|
||||
PASSWORD=$(openssl rand -base64 12)
|
||||
|
||||
if ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:latest > /dev/null 2>&1; then
|
||||
printf "${YELLOW}> Pre-built image not found, building locally...${NC}"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
|
||||
else
|
||||
(
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile . > install_build_output.log 2>&1 &
|
||||
BUILD_PID=$!
|
||||
while kill -0 $BUILD_PID 2>/dev/null; do
|
||||
printf "."
|
||||
sleep 1
|
||||
done
|
||||
printf "\n"
|
||||
wait $BUILD_PID
|
||||
BUILD_EXIT_CODE=$?
|
||||
if [ $BUILD_EXIT_CODE -ne 0 ]; then
|
||||
printf "\n${RED}> Error building Docker image. Check install_build_output.log for details.${NC}\n"
|
||||
exit $BUILD_EXIT_CODE
|
||||
fi
|
||||
)
|
||||
fi
|
||||
HASH=$(docker run --rm installcli "$PASSWORD")
|
||||
if [ -z "$HASH" ]; then
|
||||
printf "${RED}> Error: Failed to generate password hash${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
HASH=$(docker run --rm ${GITHUB_CONTAINER_REGISTRY}-installcli:latest "$PASSWORD")
|
||||
if [ -z "$HASH" ]; then
|
||||
printf "${RED}> Error: Failed to generate password hash${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$HASH" ]; then
|
||||
update_env_var "ADMIN_PASSWORD_HASH" "$HASH"
|
||||
update_env_var "ADMIN_PASSWORD_GENERATED" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
printf " ==> New admin password: $PASSWORD\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to update environment variables
|
||||
update_env_var() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
sed -i.bak "/^${key}=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
|
||||
fi
|
||||
|
||||
echo "$key=$value" >> "$ENV_FILE"
|
||||
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
|
||||
}
|
||||
|
||||
# Function to print success message
|
||||
print_success_message() {
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
printf "\n"
|
||||
printf "${GREEN}AliasVault is successfully installed!${NC}\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}To configure the server, login to the admin panel:${NC}\n"
|
||||
printf "\n"
|
||||
if [ -n "$PASSWORD" ]; then
|
||||
printf "Admin Panel: https://${HOSTNAME}/admin\n"
|
||||
printf "Username: admin\n"
|
||||
printf "Password: $PASSWORD\n"
|
||||
printf "\n"
|
||||
printf "${YELLOW}(!) Caution: Make sure to backup the above credentials in a safe place, they won't be shown again!${NC}\n"
|
||||
else
|
||||
printf "Admin Panel: https://${HOSTNAME}/admin\n"
|
||||
printf "Username: admin\n"
|
||||
printf "Password: (Previously set. Use ./install.sh reset-password to generate new one.)\n"
|
||||
fi
|
||||
printf "\n"
|
||||
printf "${CYAN}===========================${NC}\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}In order to start using AliasVault, log into the client website:${NC}\n"
|
||||
printf "\n"
|
||||
printf "Client Website: https://${HOSTNAME}/\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
}
|
||||
|
||||
# Function to recreate (restart) Docker containers
|
||||
recreate_docker_containers() {
|
||||
printf "${CYAN}> Recreating Docker containers...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker compose up -d --force-recreate
|
||||
else
|
||||
docker compose up -d --force-recreate > /dev/null 2>&1
|
||||
fi
|
||||
printf "${GREEN}> Docker containers recreated.${NC}\n"
|
||||
}
|
||||
|
||||
# Function to print password reset success message
|
||||
print_password_reset_message() {
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
printf "\n"
|
||||
printf "${GREEN}The admin password is successfully reset, see the output above. You can now login to the admin panel using this new password.${NC}\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# Function to get docker compose command with appropriate config files
|
||||
get_docker_compose_command() {
|
||||
local base_command="docker compose -f docker-compose.yml"
|
||||
|
||||
# Check if using build configuration
|
||||
if [ "$1" = "build" ]; then
|
||||
base_command="$base_command -f docker-compose.build.yml"
|
||||
fi
|
||||
|
||||
# Check if Let's Encrypt is enabled
|
||||
if grep -q "^LETSENCRYPT_ENABLED=true" "$ENV_FILE" 2>/dev/null; then
|
||||
base_command="$base_command -f docker-compose.letsencrypt.yml"
|
||||
fi
|
||||
|
||||
echo "$base_command"
|
||||
}
|
||||
|
||||
# Function to handle installation
|
||||
handle_install() {
|
||||
printf "${YELLOW}+++ Installing AliasVault +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Initialize workspace which makes sure all required directories and files exist
|
||||
initialize_workspace
|
||||
|
||||
# Initialize environment
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
|
||||
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
|
||||
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
|
||||
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
|
||||
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
|
||||
# Only generate admin password if not already set
|
||||
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
|
||||
fi
|
||||
|
||||
# Pull images from GitHub Container Registry
|
||||
printf "\n${YELLOW}+++ Pulling Docker images +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
images=(
|
||||
"${GITHUB_CONTAINER_REGISTRY}-reverse-proxy:latest"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-api:latest"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-client:latest"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-admin:latest"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-smtp:latest"
|
||||
)
|
||||
|
||||
for image in "${images[@]}"; do
|
||||
printf "${CYAN}> Pulling $image...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker pull $image || { printf "${RED}> Failed to pull image: $image${NC}\n"; exit 1; }
|
||||
else
|
||||
docker pull $image > /dev/null 2>&1 || { printf "${RED}> Failed to pull image: $image${NC}\n"; exit 1; }
|
||||
fi
|
||||
done
|
||||
|
||||
# Start containers
|
||||
printf "\n${YELLOW}+++ Starting services +++${NC}\n"
|
||||
printf "\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
$(get_docker_compose_command) up -d || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
|
||||
else
|
||||
$(get_docker_compose_command) up -d > /dev/null 2>&1 || { printf "${RED}> Failed to start Docker containers${NC}\n"; exit 1; }
|
||||
fi
|
||||
|
||||
# Only show success message if we made it here without errors
|
||||
print_success_message
|
||||
}
|
||||
|
||||
# Function to handle build
|
||||
handle_build() {
|
||||
printf "${YELLOW}+++ Building AliasVault from source +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check for required build files
|
||||
if [ ! -f "docker-compose.build.yml" ] || [ ! -d "src" ]; then
|
||||
printf "${RED}Error: Required files for building from source are missing.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To build AliasVault from source, you need:\n"
|
||||
printf "1. docker-compose.build.yml file\n"
|
||||
printf "2. src/ directory with the complete source code\n"
|
||||
printf "\n"
|
||||
printf "Please clone the complete repository using:\n"
|
||||
printf "git clone https://github.com/${REPO_OWNER}/${REPO_NAME}.git\n"
|
||||
printf "\n"
|
||||
printf "Alternatively, you can use '/install' to pull pre-built images.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Initialize environment with proper error handling
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
|
||||
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
|
||||
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
|
||||
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
|
||||
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
|
||||
# Only generate admin password if not already set
|
||||
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
|
||||
fi
|
||||
|
||||
printf "\n${YELLOW}+++ Building and starting services +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
printf "${CYAN}> Building Docker Compose stack...${NC}"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
$(get_docker_compose_command "build") build || {
|
||||
printf "\n${RED}> Failed to build Docker Compose stack${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
(
|
||||
$(get_docker_compose_command "build") build > install_compose_build_output.log 2>&1 &
|
||||
BUILD_PID=$!
|
||||
while kill -0 $BUILD_PID 2>/dev/null; do
|
||||
printf "."
|
||||
sleep 1
|
||||
done
|
||||
wait $BUILD_PID
|
||||
BUILD_EXIT_CODE=$?
|
||||
if [ $BUILD_EXIT_CODE -ne 0 ]; then
|
||||
printf "\n${RED}> Failed to build Docker Compose stack. Check install_compose_build_output.log for details.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
fi
|
||||
printf "\n${GREEN}> Docker Compose stack built successfully.${NC}\n"
|
||||
|
||||
printf "${CYAN}> Starting Docker Compose stack...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
$(get_docker_compose_command "build") up -d || {
|
||||
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
$(get_docker_compose_command "build") up -d > /dev/null 2>&1 || {
|
||||
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
printf "${GREEN}> Docker Compose stack started successfully.${NC}\n"
|
||||
|
||||
# Only show success message if we made it here without errors
|
||||
print_success_message
|
||||
}
|
||||
|
||||
# Function to handle uninstall
|
||||
handle_uninstall() {
|
||||
printf "${YELLOW}+++ Uninstalling AliasVault +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check if -y flag was passed
|
||||
if [ "$FORCE_YES" != "true" ]; then
|
||||
# Ask for confirmation before proceeding
|
||||
read -p "Are you sure you want to uninstall AliasVault? This will remove all containers and images. [y/N]: " REPLY
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
printf "${YELLOW}> Uninstall cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "${CYAN}> Stopping and removing Docker containers...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker compose -f docker-compose.yml down -v || {
|
||||
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
docker compose -f docker-compose.yml down -v > /dev/null 2>&1 || {
|
||||
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
printf "${GREEN}> Docker containers stopped and removed.${NC}\n"
|
||||
|
||||
printf "${CYAN}> Removing Docker images...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker compose -f docker-compose.yml down --rmi all || {
|
||||
printf "${RED}> Failed to remove Docker images${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
docker compose -f docker-compose.yml down --rmi all > /dev/null 2>&1 || {
|
||||
printf "${RED}> Failed to remove Docker images${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
printf "${GREEN}> Docker images removed.${NC}\n"
|
||||
|
||||
printf "${CYAN}> Pruning Docker system...${NC}\n"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker system prune -af || {
|
||||
printf "${RED}> Failed to prune Docker system${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
docker system prune -af > /dev/null 2>&1 || {
|
||||
printf "${RED}> Failed to prune Docker system${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
printf "${GREEN}> Docker system pruned.${NC}\n"
|
||||
|
||||
# Only show success message if we made it here without errors
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
printf "\n"
|
||||
printf "${GREEN}AliasVault has been successfully uninstalled!${NC}\n"
|
||||
printf "\n"
|
||||
printf "All Docker containers and images related to AliasVault have been removed.\n"
|
||||
printf "The current directory, including logs and .env files, has been left intact.\n"
|
||||
printf "\n"
|
||||
printf "If you wish to remove the remaining files, you can do so manually.\n"
|
||||
printf "\n"
|
||||
printf "Thank you for using AliasVault!\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
}
|
||||
|
||||
# Function to handle SSL configuration
|
||||
handle_ssl_configuration() {
|
||||
printf "${YELLOW}+++ SSL Certificate Configuration +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check if AliasVault is installed
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current hostname and SSL config from .env
|
||||
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
LETSENCRYPT_ENABLED=$(grep "^LETSENCRYPT_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
|
||||
printf "${CYAN}About SSL Certificates:${NC}\n"
|
||||
printf "A default installation of AliasVault comes with a self-signed SSL certificate.\n"
|
||||
printf "While self-signed certificates provide encryption, they will show security warnings in browsers.\n"
|
||||
printf "\n"
|
||||
printf "AliasVault also supports generating valid SSL certificates via Let's Encrypt.\n"
|
||||
printf "Let's Encrypt certificates are trusted by browsers and will not show security warnings.\n"
|
||||
printf "However, Let's Encrypt requires that:\n"
|
||||
printf " - AliasVault is reachable from the internet via port 80/443\n"
|
||||
printf " - You have configured a valid domain name (not localhost)\n"
|
||||
printf "\n"
|
||||
printf "Let's Encrypt certificates will be automatically renewed before expiry.\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}Current Configuration:${NC}\n"
|
||||
if [ "$LETSENCRYPT_ENABLED" = "true" ]; then
|
||||
printf "Currently using: ${GREEN}Let's Encrypt certificates${NC}\n"
|
||||
else
|
||||
printf "Currently using: ${YELLOW}Self-signed certificates${NC}\n"
|
||||
fi
|
||||
|
||||
printf "Current hostname: ${CYAN}${CURRENT_HOSTNAME}${NC}\n"
|
||||
printf "\n"
|
||||
printf "SSL Options:\n"
|
||||
printf "1) Activate and/or request new Let's Encrypt certificate (recommended for production)\n"
|
||||
printf "2) Activate and/or generate new self-signed certificate\n"
|
||||
printf "3) Cancel\n"
|
||||
printf "\n"
|
||||
|
||||
read -p "Select an option [1-3]: " ssl_option
|
||||
|
||||
case $ssl_option in
|
||||
1)
|
||||
configure_letsencrypt
|
||||
;;
|
||||
2)
|
||||
generate_self_signed_cert
|
||||
;;
|
||||
3)
|
||||
printf "${YELLOW}SSL configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf "${RED}Invalid option selected.${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to configure Let's Encrypt
|
||||
configure_letsencrypt() {
|
||||
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
|
||||
|
||||
# Check if hostname is localhost
|
||||
if [ "$CURRENT_HOSTNAME" = "localhost" ]; then
|
||||
printf "${RED}Error: Let's Encrypt certificates cannot be issued for 'localhost'.${NC}\n"
|
||||
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if hostname is a valid domain
|
||||
if ! [[ "$CURRENT_HOSTNAME" =~ \.[a-zA-Z]{2,}$ ]]; then
|
||||
printf "${RED}Error: Invalid hostname '${CURRENT_HOSTNAME}'.${NC}\n"
|
||||
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify DNS is properly configured
|
||||
printf "\n${YELLOW}Important: Before proceeding, ensure that:${NC}\n"
|
||||
printf "1. AliasVault is currently running and accessible at ${CYAN}https://${CURRENT_HOSTNAME}${NC}\n"
|
||||
printf "2. Your domain (${CYAN}${CURRENT_HOSTNAME}${NC}) is externally resolvable to this server's IP address\n"
|
||||
printf "3. Ports 80 and 443 are open and accessible from the internet\n"
|
||||
printf "\n"
|
||||
|
||||
read -p "Have you completed these steps? [y/N]: " REPLY
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
printf "${YELLOW}> Let's Encrypt configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get contact email for Let's Encrypt
|
||||
SUPPORT_EMAIL=$(grep "^SUPPORT_EMAIL=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
LETSENCRYPT_EMAIL=""
|
||||
|
||||
while true; do
|
||||
printf "\nPlease enter a valid email address that will be used for Let's Encrypt certificate notifications:\n"
|
||||
read -p "Email: " LETSENCRYPT_EMAIL
|
||||
if [[ "$LETSENCRYPT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
|
||||
printf "Confirm using ${CYAN}${LETSENCRYPT_EMAIL}${NC} for Let's Encrypt notifications? [y/N] "
|
||||
read REPLY
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
break
|
||||
fi
|
||||
else
|
||||
printf "${RED}Invalid email format. Please try again.${NC}\n"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create certbot directories
|
||||
printf "${CYAN}> Creating Let's Encrypt directories...${NC}\n"
|
||||
mkdir -p ./certificates/letsencrypt/www
|
||||
|
||||
# Request certificate using a temporary certbot container
|
||||
printf "${CYAN}> Requesting Let's Encrypt certificate...${NC}\n"
|
||||
docker run --rm \
|
||||
--network aliasvault_default \
|
||||
-v ./certificates/letsencrypt:/etc/letsencrypt:rw \
|
||||
-v ./certificates/letsencrypt/www:/var/www/certbot:rw \
|
||||
certbot/certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email "$LETSENCRYPT_EMAIL" \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--non-interactive \
|
||||
--domains ${CURRENT_HOSTNAME} \
|
||||
--force-renewal
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "${RED}Failed to obtain Let's Encrypt certificate.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fix permissions on Let's Encrypt directories and files
|
||||
sudo chmod -R 755 ./certificates/letsencrypt
|
||||
|
||||
# Ensure private keys remain secure
|
||||
sudo find ./certificates/letsencrypt -type f -name "privkey*.pem" -exec chmod 600 {} \;
|
||||
sudo find ./certificates/letsencrypt -type f -name "fullchain*.pem" -exec chmod 644 {} \;
|
||||
|
||||
# Update .env to indicate Let's Encrypt is enabled
|
||||
update_env_var "LETSENCRYPT_ENABLED" "true"
|
||||
|
||||
# Restart only the reverse proxy with new configuration so it loads the new certificate
|
||||
printf "${CYAN}> Restarting reverse proxy with Let's Encrypt configuration...${NC}\n"
|
||||
$(get_docker_compose_command) up -d reverse-proxy
|
||||
|
||||
printf "${GREEN}> Let's Encrypt SSL certificate has been configured successfully!${NC}\n"
|
||||
}
|
||||
|
||||
# Function to generate self-signed certificate
|
||||
generate_self_signed_cert() {
|
||||
printf "${CYAN}> Generating new self-signed certificate...${NC}\n"
|
||||
|
||||
# Disable Let's Encrypt
|
||||
update_env_var "LETSENCRYPT_ENABLED" "false"
|
||||
|
||||
# Stop existing containers
|
||||
printf "${CYAN}> Stopping existing containers...${NC}\n"
|
||||
docker compose down
|
||||
|
||||
# Remove existing certificates
|
||||
rm -f ./certificates/ssl/cert.pem ./certificates/ssl/key.pem
|
||||
|
||||
# Remove Let's Encrypt directories
|
||||
rm -rf ./certificates/letsencrypt
|
||||
|
||||
# Start containers (which will generate new self-signed certs)
|
||||
printf "${CYAN}> Restarting services...${NC}\n"
|
||||
docker compose up -d
|
||||
|
||||
printf "${GREEN}> New self-signed certificate has been generated successfully!${NC}\n"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
100
nginx.conf
Normal file
100
nginx.conf
Normal file
@@ -0,0 +1,100 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream client {
|
||||
server client:3000;
|
||||
}
|
||||
|
||||
upstream api {
|
||||
server api:3001;
|
||||
}
|
||||
|
||||
upstream admin {
|
||||
server admin:3002;
|
||||
}
|
||||
|
||||
# Preserve any existing X-Forwarded-* headers, this is relevant if AliasVault
|
||||
# is running behind another reverse proxy.
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 192.168.0.0/16;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Enable gzip compression, which reduces the amount of data that needs to be transferred
|
||||
# to speed up WASM load times.
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Handle ACME challenge for Let's Encrypt certificate validation
|
||||
location /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
root /var/www/certbot;
|
||||
try_files $uri =404;
|
||||
default_type "text/plain";
|
||||
add_header Cache-Control "no-cache";
|
||||
break;
|
||||
}
|
||||
|
||||
# Redirect all other HTTP traffic to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
# Include the appropriate SSL certificate configuration generated
|
||||
# by the entrypoint script.
|
||||
include /etc/nginx/ssl.conf;
|
||||
|
||||
# Admin interface
|
||||
location /admin {
|
||||
proxy_pass http://admin;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# Add WebSocket support for Blazor server
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# API endpoints
|
||||
location /api {
|
||||
proxy_pass http://api;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Client app (root path)
|
||||
location / {
|
||||
proxy_pass http://client;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AliasDbContext.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasDb;
|
||||
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The AliasDbContext class.
|
||||
/// </summary>
|
||||
public class AliasDbContext : IdentityDbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AliasDbContext"/> class.
|
||||
/// </summary>
|
||||
public AliasDbContext()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AliasDbContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">DbContextOptions.</param>
|
||||
public AliasDbContext(DbContextOptions<AliasDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Identities DbSet.
|
||||
/// </summary>
|
||||
public DbSet<Identity> Identities { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Logins DbSet.
|
||||
/// </summary>
|
||||
public DbSet<Login> Logins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Passwords DbSet.
|
||||
/// </summary>
|
||||
public DbSet<Password> Passwords { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Services DbSet.
|
||||
/// </summary>
|
||||
public DbSet<Service> Services { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AspNetUserRefreshTokens DbSet.
|
||||
/// </summary>
|
||||
public DbSet<AspNetUserRefreshToken> AspNetUserRefreshTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
/// <param name="builder">ModelBuilder instance.</param>
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
foreach (var entity in builder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entity.GetProperties())
|
||||
{
|
||||
// NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
|
||||
// NOTE: SQL server doesn't need this override.
|
||||
|
||||
// SQLite does not support varchar(max) so we use TEXT.
|
||||
if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
|
||||
{
|
||||
property.SetColumnType("TEXT");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Identity - Login relationship
|
||||
builder.Entity<Login>()
|
||||
.HasOne(l => l.Identity)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.IdentityId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure the Login - UserId entity
|
||||
builder.Entity<Login>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
|
||||
// Configure Login - Service relationship
|
||||
builder.Entity<Login>()
|
||||
.HasOne(l => l.Service)
|
||||
.WithMany()
|
||||
.HasForeignKey(l => l.ServiceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure Login - Password relationship
|
||||
builder.Entity<Login>()
|
||||
.HasMany(l => l.Passwords)
|
||||
.WithOne(p => p.Login)
|
||||
.HasForeignKey(p => p.LoginId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Configure Identity - DefaultPassword relationship
|
||||
builder.Entity<Identity>()
|
||||
.HasOne(i => i.DefaultPassword)
|
||||
.WithMany()
|
||||
.HasForeignKey(i => i.DefaultPasswordId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Configure the User - AspNetUserRefreshToken entity
|
||||
builder.Entity<AspNetUserRefreshToken>()
|
||||
.HasOne(p => p.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(p => p.UserId)
|
||||
.IsRequired();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the connection string if it is not already configured.
|
||||
/// </summary>
|
||||
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
// If the options are not already configured, use the appsettings.json file.
|
||||
if (!optionsBuilder.IsConfigured)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
optionsBuilder
|
||||
.UseSqlite(configuration.GetConnectionString("AliasDbContext"))
|
||||
.UseLazyLoadingProxies();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Identity.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasDb;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// The identity entity.
|
||||
/// </summary>
|
||||
public class Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identity primary key.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gender.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first name.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? FirstName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last name.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? LastName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nickname.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? NickName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the birth date.
|
||||
/// </summary>
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address street.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressStreet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address city.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressCity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address state.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address zip code.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressZipCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address country.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressCountry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hobbies in CSV format, can contain multiple values separated by ";".
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? Hobbies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated email prefix.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? EmailPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the random generated mobile phone number.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? PhoneMobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated IBAN bank account number.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? BankAccountIBAN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created timestamp.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the updated timestamp.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the login foreign key.
|
||||
/// </summary>
|
||||
public Guid? DefaultPasswordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the login navigation property.
|
||||
/// </summary>
|
||||
[ForeignKey("DefaultPasswordId")]
|
||||
public virtual Password? DefaultPassword { get; set; }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FigIdentityGenerator.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasGenerators.Identity.Implementations;
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Identity generator which generates random identities using the identiteitgenerator.nl semi-public API.
|
||||
/// </summary>
|
||||
public class FigIdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
private static readonly string Url = "https://api.identiteitgenerator.nl/generate/identity";
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(Url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, JsonSerializerOptions);
|
||||
|
||||
if (identity is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize the identity from FIG WebApi.");
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="StaticIdentityGenerator.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasGenerators.Identity.Implementations;
|
||||
|
||||
using AliasGenerators.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Static identity generator which implements IIdentityGenerator but always returns
|
||||
/// the same static identity for testing purposes.
|
||||
/// </summary>
|
||||
public class StaticIdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
await Task.Yield(); // Add an await statement to make the method truly asynchronous.
|
||||
return new Identity.Models.Identity
|
||||
{
|
||||
FirstName = "John",
|
||||
LastName = "Doe",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Address.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Address model.
|
||||
/// </summary>
|
||||
public class Address
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the street.
|
||||
/// </summary>
|
||||
public string Street { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
public string City { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
public string State { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the zip code.
|
||||
/// </summary>
|
||||
public string ZipCode { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
public string Country { get; set; } = null!;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Identity.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Identity model.
|
||||
/// </summary>
|
||||
public class Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gender.
|
||||
/// </summary>
|
||||
public int Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first name.
|
||||
/// </summary>
|
||||
public string FirstName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last name.
|
||||
/// </summary>
|
||||
public string LastName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nickname. This is also used as the username.
|
||||
/// </summary>
|
||||
public string NickName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the birth date.
|
||||
/// </summary>
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address.
|
||||
/// </summary>
|
||||
public Address Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the job.
|
||||
/// </summary>
|
||||
public Job Job { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hobbies.
|
||||
/// </summary>
|
||||
public List<string> Hobbies { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email address prefix.
|
||||
/// </summary>
|
||||
public string EmailPrefix { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the phone mobile.
|
||||
/// </summary>
|
||||
public string PhoneMobile { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bank account IBAN.
|
||||
/// </summary>
|
||||
public string BankAccountIBAN { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile photo in base64 format.
|
||||
/// </summary>
|
||||
public string ProfilePhotoBase64 { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile photo prompt.
|
||||
/// </summary>
|
||||
public string ProfilePhotoPrompt { get; set; } = null!;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Job.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
namespace AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Job model.
|
||||
/// </summary>
|
||||
public class Job
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the company.
|
||||
/// </summary>
|
||||
public string Company { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salary.
|
||||
/// </summary>
|
||||
public string Salary { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the calculated salary.
|
||||
/// </summary>
|
||||
public decimal SalaryCalculated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = null!;
|
||||
}
|
||||
53
src/AliasVault.Admin/AliasVault.Admin.csproj
Normal file
53
src/AliasVault.Admin/AliasVault.Admin.csproj
Normal file
@@ -0,0 +1,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>aspnet-AliasVault.Admin-1DAADE35-C01B-43BB-B440-AA5E1E0B672D</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<NoWarn>1701;1702;NU1900</NoWarn>
|
||||
<LangVersion>13</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\net9.0\AliasVault.Admin.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\Release\net9.0\AliasVault.Admin.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Auth\AliasVault.Auth.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Logging\AliasVault.Logging.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -12,30 +12,36 @@
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Id { get; set; } = null!;
|
||||
[Parameter]
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Value { get; set; } = null!;
|
||||
[Parameter]
|
||||
public required string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
||||
[Parameter]
|
||||
public required EventCallback<string?> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expression that identifies the value property.
|
||||
/// </summary>
|
||||
[Parameter] public Expression<Func<string>> ValueExpression { get; set; } = null!;
|
||||
[Parameter]
|
||||
public required Expression<Func<string>> ValueExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter] public string Placeholder { get; set; } = null!;
|
||||
[Parameter]
|
||||
public required string Placeholder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional attributes for the input field.
|
||||
/// </summary>
|
||||
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
|
||||
}
|
||||
10
src/AliasVault.Admin/Auth/Components/Logo.razor
Normal file
10
src/AliasVault.Admin/Auth/Components/Logo.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using AliasVault.Admin.Services
|
||||
@inject NavigationService NavigationService
|
||||
|
||||
<a href="@NavigationService.BaseUri">
|
||||
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
|
||||
<span>AliasVault</span>
|
||||
<span class="ps-2 self-center hidden sm:flex text-lg font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
</div>
|
||||
</a>
|
||||
36
src/AliasVault.Admin/Auth/Layout/AuthLayout.razor
Normal file
36
src/AliasVault.Admin/Auth/Layout/AuthLayout.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using AliasVault.Admin.Auth.Components
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-6 pt-8 mx-auto md:h-screen pt:mt-0 dark:bg-gray-900">
|
||||
<Logo />
|
||||
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
18
src/AliasVault.Admin/Auth/Layout/AuthLayout.razor.css
Normal file
18
src/AliasVault.Admin/Auth/Layout/AuthLayout.razor.css
Normal file
@@ -0,0 +1,18 @@
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
78
src/AliasVault.Admin/Auth/Pages/AuthBase.cs
Normal file
78
src/AliasVault.Admin/Auth/Pages/AuthBase.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AuthBase.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Admin.Auth.Pages;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Admin.Main.Components.Alerts;
|
||||
using AliasVault.Admin.Services;
|
||||
using AliasVault.Auth;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Base auth page that all pages that are part of the auth (non-logged in part of website) should inherit from.
|
||||
/// All pages that inherit from this class will require the user to be logged out. If user is logged in they
|
||||
/// are automatically redirected to index page.
|
||||
/// </summary>
|
||||
public class AuthBase : OwningComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the logger.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected ILogger<Login> Logger { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the navigation service.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected NavigationService NavigationService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sign in manager.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected SignInManager<AdminUser> SignInManager { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user manager.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected UserManager<AdminUser> UserManager { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication state provider.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the auth logging service.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected AuthLoggingService AuthLoggingService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets object which holds server validation errors to show in the UI.
|
||||
/// </summary>
|
||||
protected ServerValidationErrors ServerValidationErrors { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
|
||||
// Redirect to home if the user is already authenticated
|
||||
if (SignInManager.IsSignedIn(user))
|
||||
{
|
||||
NavigationService.RedirectTo("./");
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/AliasVault.Admin/Auth/Pages/ForgotPassword.razor
Normal file
8
src/AliasVault.Admin/Auth/Pages/ForgotPassword.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/user/forgot-password"
|
||||
|
||||
<LayoutPageTitle>Forgot your password?</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Forgot your password?
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">If you have forgotten your password, contact the server admin or consult the AliasVault documentation on how to reset your password.</p>
|
||||
8
src/AliasVault.Admin/Auth/Pages/Lockout.razor
Normal file
8
src/AliasVault.Admin/Auth/Pages/Lockout.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@page "/user/lockout"
|
||||
|
||||
<LayoutPageTitle>Locked out</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Locked out
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">You have entered an incorrect password too many times and your account has now been locked out. You can try again in 30 minutes.</p>
|
||||
112
src/AliasVault.Admin/Auth/Pages/Login.razor
Normal file
112
src/AliasVault.Admin/Auth/Pages/Login.razor
Normal file
@@ -0,0 +1,112 @@
|
||||
@page "/user/login"
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
|
||||
<LayoutPageTitle>Log in</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Sign in to AliasVault Admin
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<EditForm Model="Input" FormName="LoginForm" OnValidSubmit="LoginUser" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.UserName" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username</label>
|
||||
<InputTextField id="username" @bind-Value="Input.UserName" placeholder="username" />
|
||||
<ValidationMessage For="() => Input.UserName"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="Input.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => Input.Password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="remember" aria-describedby="remember" name="remember" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600">
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
|
||||
</div>
|
||||
<a href="user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>
|
||||
</EditForm>
|
||||
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
if (HttpMethods.IsGet(HttpContext.Request.Method))
|
||||
{
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs in the user.
|
||||
/// </summary>
|
||||
protected async Task LoginUser()
|
||||
{
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
var user = await UserManager.FindByNameAsync(Input.UserName);
|
||||
if (user == null)
|
||||
{
|
||||
|
||||
await AuthLoggingService.LogAuthEventFailAsync(Input.UserName, AuthEventType.Login, AuthFailureReason.InvalidUsername);
|
||||
ServerValidationErrors.AddError("Error: Invalid login attempt.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await SignInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(Input.UserName, AuthEventType.Login);
|
||||
Logger.LogInformation("User logged in.");
|
||||
NavigationService.RedirectTo(ReturnUrl ?? "/");
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(Input.UserName, AuthEventType.Login);
|
||||
NavigationService.RedirectTo(
|
||||
"user/loginWith2fa",
|
||||
new Dictionary<string, object?> { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(Input.UserName, AuthEventType.Login, AuthFailureReason.AccountLocked);
|
||||
Logger.LogWarning("User account locked out.");
|
||||
NavigationService.RedirectTo("user/lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(Input.UserName, AuthEventType.Login, AuthFailureReason.InvalidPassword);
|
||||
ServerValidationErrors.AddError("Error: Invalid login attempt.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required] public string UserName { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; } = true;
|
||||
}
|
||||
|
||||
}
|
||||
105
src/AliasVault.Admin/Auth/Pages/LoginWith2fa.razor
Normal file
105
src/AliasVault.Admin/Auth/Pages/LoginWith2fa.razor
Normal file
@@ -0,0 +1,105 @@
|
||||
@page "/user/loginWith2fa"
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
|
||||
<LayoutPageTitle>Two-factor authentication</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="login-with-2fa" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<input type="hidden" name="ReturnUrl" value="@ReturnUrl"/>
|
||||
<input type="hidden" name="RememberMe" value="@RememberMe"/>
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
|
||||
<InputText @bind-Value="Input.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => Input.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<InputCheckbox @bind-Value="Input.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember-machine" class="font-medium text-gray-900 dark:text-white">Remember this machine</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
Don't have access to your authenticator device? You can
|
||||
<a href="user/loginWithRecoveryCode?ReturnUrl=@ReturnUrl" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</a>.
|
||||
</p>
|
||||
|
||||
@code {
|
||||
private AdminUser user = default!;
|
||||
|
||||
[SupplyParameterFromForm]
|
||||
private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
private string? ReturnUrl { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
private bool RememberMe { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the form.
|
||||
/// </summary>
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine);
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TwoFactorAuthentication);
|
||||
Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId);
|
||||
NavigationService.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TwoFactorAuthentication, AuthFailureReason.AccountLocked);
|
||||
Logger.LogWarning("User with ID '{UserId}' account locked out.", userId);
|
||||
NavigationService.RedirectTo("user/lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TwoFactorAuthentication, AuthFailureReason.InvalidTwoFactorCode);
|
||||
Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId);
|
||||
ServerValidationErrors.AddError("Error: Invalid authenticator code.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Authenticator code")]
|
||||
public string? TwoFactorCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember this machine")]
|
||||
public bool RememberMachine { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
83
src/AliasVault.Admin/Auth/Pages/LoginWithRecoveryCode.razor
Normal file
83
src/AliasVault.Admin/Auth/Pages/LoginWithRecoveryCode.razor
Normal file
@@ -0,0 +1,83 @@
|
||||
@page "/user/loginWithRecoveryCode"
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
|
||||
<LayoutPageTitle>Recovery code verification</LayoutPageTitle>
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Recovery code verification
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
You have requested to log in with a recovery code. A recovery code is a one-time code that can be used to log in to your account.
|
||||
Note that if you don't manually disable 2FA after login, you will be asked for an authenticator code again at the next login.
|
||||
</p>
|
||||
<div class="w-full max-w-md">
|
||||
<EditForm Model="Input" FormName="login-with-recovery-code" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="recovery-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Recovery Code</label>
|
||||
<InputText @bind-Value="Input.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off" placeholder="Enter your recovery code"/>
|
||||
<ValidationMessage For="() => Input.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AdminUser user = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
[SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ??
|
||||
throw new InvalidOperationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the form.
|
||||
/// </summary>
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty);
|
||||
var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
var userId = await UserManager.GetUserIdAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TwoFactorAuthentication);
|
||||
Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId);
|
||||
NavigationService.RedirectTo(ReturnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TwoFactorAuthentication, AuthFailureReason.AccountLocked);
|
||||
Logger.LogWarning("User account locked out.");
|
||||
NavigationService.RedirectTo("user/lockout");
|
||||
}
|
||||
else
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TwoFactorAuthentication, AuthFailureReason.InvalidRecoveryCode);
|
||||
Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId);
|
||||
ServerValidationErrors.AddError("Error: Invalid recovery code entered.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Recovery Code")]
|
||||
public string RecoveryCode { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
40
src/AliasVault.Admin/Auth/Pages/Logout.razor
Normal file
40
src/AliasVault.Admin/Auth/Pages/Logout.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@page "/user/logout"
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@inject UserService UserService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Sign out the user.
|
||||
// NOTE: the try/catch below is a workaround for the issue that the sign out does not work when
|
||||
// the server session is already started.
|
||||
try
|
||||
{
|
||||
var username = UserService.User().UserName;
|
||||
try
|
||||
{
|
||||
await SignInManager.SignOutAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(username!, AuthEventType.Logout);
|
||||
|
||||
// Redirect to the home page with hard refresh.
|
||||
NavigationService.RedirectTo("/", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hard refresh current page if sign out fails. When an interactive server session is already started
|
||||
// the sign out will fail because it tries to mutate cookies which is only possible when the server
|
||||
// session is not started yet.
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(username!, AuthEventType.Logout);
|
||||
NavigationService.RedirectTo(NavigationService.Uri, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Redirect to the home page with hard refresh.
|
||||
NavigationService.RedirectTo("./", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/AliasVault.Admin/Auth/Pages/_Imports.razor
Normal file
11
src/AliasVault.Admin/Auth/Pages/_Imports.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@inherits AuthBase
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using AliasVault.Admin.Auth.Components
|
||||
@using AliasVault.Admin.Auth.Layout
|
||||
@using AliasVault.Admin.Main.Components.Alerts
|
||||
@using AliasVault.Admin.Main.Components.Layout
|
||||
@using AliasVault.Admin.Main.Layout
|
||||
@using AliasVault.Admin.Services
|
||||
@using Microsoft.AspNetCore.Authentication
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@layout AuthLayout
|
||||
@@ -0,0 +1,67 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RevalidatingAuthenticationStateProvider.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Admin.Auth.Providers;
|
||||
|
||||
using System.Security.Claims;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
/// <summary>
|
||||
/// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
|
||||
/// every 30 minutes an interactive circuit is connected.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">ILoggerFactory instance.</param>
|
||||
/// <param name="scopeFactory">IServiceScopeFactory instance.</param>
|
||||
/// <param name="options">IOptions instance.</param>
|
||||
internal sealed class RevalidatingAuthenticationStateProvider(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<IdentityOptions> options)
|
||||
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the revalidation interval.
|
||||
/// </summary>
|
||||
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
|
||||
|
||||
/// <summary>
|
||||
/// Validate the authentication state.
|
||||
/// </summary>
|
||||
/// <param name="authenticationState">AuthenticationState instance.</param>
|
||||
/// <param name="cancellationToken">CancellationToken.</param>
|
||||
/// <returns>Boolean indicating whether the currently logged on user is still valid.</returns>
|
||||
protected override async Task<bool> ValidateAuthenticationStateAsync(
|
||||
AuthenticationState authenticationState, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get the user manager from a new scope to ensure it fetches fresh data
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AdminUser>>();
|
||||
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateSecurityStampAsync(UserManager<AdminUser> userManager, ClaimsPrincipal principal)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(principal);
|
||||
if (user is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!userManager.SupportsUserSecurityStamp)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
|
||||
var userStamp = await userManager.GetSecurityStampAsync(user);
|
||||
return principalStamp == userStamp;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@@ -7,7 +7,6 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using AliasVault
|
||||
@using AliasVault.Components
|
||||
@using AliasVault.Components.Shared
|
||||
@using AliasVault.Components.Pages.Aliases
|
||||
@using AliasVault.Admin
|
||||
@using AliasVault.Admin.Main
|
||||
@using AliasServerDb
|
||||
26
src/AliasVault.Admin/Config.cs
Normal file
26
src/AliasVault.Admin/Config.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Config.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration class for the Admin project with values loaded from environment variables.
|
||||
/// </summary>
|
||||
public class Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the admin password hash which is generated by install.sh and will be set
|
||||
/// as the default password for the admin user.
|
||||
/// </summary>
|
||||
public string AdminPasswordHash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last time the password was changed. This is used to check if the
|
||||
/// password hash generated by install.sh should replace the current password hash if user already exists.
|
||||
/// </summary>
|
||||
public DateTime LastPasswordChanged { get; set; } = DateTime.MinValue;
|
||||
}
|
||||
26
src/AliasVault.Admin/Dockerfile
Normal file
26
src/AliasVault.Admin/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 3002
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
|
||||
WORKDIR /src
|
||||
COPY ["src/AliasVault.Admin/AliasVault.Admin.csproj", "src/AliasVault.Admin/"]
|
||||
RUN dotnet restore "src/AliasVault.Admin/AliasVault.Admin.csproj"
|
||||
COPY . .
|
||||
|
||||
WORKDIR "/src/src/AliasVault.Admin"
|
||||
RUN dotnet build "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:3002
|
||||
ENV ASPNETCORE_PATHBASE=/admin
|
||||
ENTRYPOINT ["dotnet", "AliasVault.Admin.dll"]
|
||||
31
src/AliasVault.Admin/Main/App.razor
Normal file
31
src/AliasVault.Admin/Main/App.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
@inject VersionedContentService VersionService
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||
<base href="@NavigationService.BaseUri"/>
|
||||
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/tailwind.css")"/>
|
||||
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/app.css")"/>
|
||||
<link rel="stylesheet" href="AliasVault.Admin.styles.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet @rendermode="RenderModeForPage"/>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-900">
|
||||
<Routes @rendermode="RenderModeForPage"/>
|
||||
<script src="@VersionService.GetVersionedPath("lib/qrcode.min.js")"></script>
|
||||
<script src="@VersionService.GetVersionedPath("js/dark-mode.js")"></script>
|
||||
<script src="@VersionService.GetVersionedPath("js/utilities.js")"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/user")
|
||||
? null
|
||||
: InteractiveServer;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@if (Messages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="messages-container grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "success")
|
||||
{
|
||||
<AlertMessageSuccess Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "info")
|
||||
{
|
||||
<AlertMessageInfo Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "warning")
|
||||
{
|
||||
<AlertMessageWarning Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "error")
|
||||
{
|
||||
<AlertMessageError Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.messages-container > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private List<KeyValuePair<string, string>> Messages { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
RefreshAddMessages();
|
||||
GlobalNotificationService.OnChange += RefreshAddMessages;
|
||||
NavigationManager.LocationChanged += HandleLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
GlobalNotificationService.OnChange -= RefreshAddMessages;
|
||||
NavigationManager.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the messages on navigation to another page.
|
||||
/// </summary>
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
RefreshAddMessages();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the messages by adding any new messages from the PortalMessageService.
|
||||
/// </summary>
|
||||
private void RefreshAddMessages()
|
||||
{
|
||||
// We retrieve any additional messages from the GlobalNotificationService that we do not yet have.
|
||||
var newMessages = GlobalNotificationService.GetMessagesForDisplay();
|
||||
foreach (var message in newMessages)
|
||||
{
|
||||
if (!Messages.Exists(m => m.Key == message.Key && m.Value == message.Value))
|
||||
{
|
||||
Messages.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove messages that are no longer in the GlobalNotificationService and have already been displayed.
|
||||
var messagesToRemove = Messages.Where(m => !newMessages.Exists(nm => nm.Key == m.Key && nm.Value == m.Value)).ToList();
|
||||
foreach (var message in messagesToRemove)
|
||||
{
|
||||
Messages.Remove(message);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@if (_errors.Any())
|
||||
{
|
||||
@foreach (var error in _errors)
|
||||
{
|
||||
<AlertMessageError Message="@error" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<string> _errors = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a server validation error.
|
||||
/// </summary>
|
||||
public void AddError(string error)
|
||||
{
|
||||
_errors.Add(error);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the server validation errors.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_errors.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<PageTitle>@ChildContent - AliasVault Admin</PageTitle>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Child content.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; } = default!;
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
<div class="loading" style="display:@(IsVisible ? "block" : "none");">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
@if (IsVisible)
|
||||
{
|
||||
<div class="loading z-50">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool IsVisible { get; set; }
|
||||
@@ -0,0 +1,22 @@
|
||||
<div role="status" class="px-2" title="@Title">
|
||||
<svg aria-hidden="true" class="inline w-7 h-7 text-gray-200 @(Spinning ? "animate-spin fill-primary-600" : "") dark:text-gray-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Optional title of the loading indicator.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Set spinning to false to stop the animation.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Spinning { get; set; } = true;
|
||||
|
||||
}
|
||||
17
src/AliasVault.Admin/Main/Components/RedirectToLogin.razor
Normal file
17
src/AliasVault.Admin/Main/Components/RedirectToLogin.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var returnUrl = NavigationManager.Uri;
|
||||
if (string.IsNullOrWhiteSpace(returnUrl) || returnUrl == "/")
|
||||
{
|
||||
NavigationManager.NavigateTo($"user/login", forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationManager.NavigateTo($"user/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
src/AliasVault.Admin/Main/Components/WorkerStatus/Services.razor
Normal file
209
src/AliasVault.Admin/Main/Components/WorkerStatus/Services.razor
Normal file
@@ -0,0 +1,209 @@
|
||||
@using AliasVault.WorkerStatus.Database
|
||||
@inherits MainBase
|
||||
|
||||
<button @onclick="SmtpClick"
|
||||
class="@GetSmtpButtonClasses() mx-3 inline-flex items-center justify-center rounded-xl px-8 py-2 text-white"
|
||||
disabled="@(!IsHeartbeatValid())"
|
||||
title="@GetButtonTooltip()">
|
||||
<span>SmtpService</span>
|
||||
@if (SmtpPending)
|
||||
{
|
||||
<svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
@code {
|
||||
private List<WorkerServiceStatus> ServiceStatus = [];
|
||||
private bool InitInProgress;
|
||||
private bool SmtpStatus;
|
||||
private bool SmtpPending;
|
||||
private DateTime LastHeartbeat;
|
||||
|
||||
/// <summary>
|
||||
/// The interval in milliseconds for refreshing the service status.
|
||||
/// </summary>
|
||||
private readonly int AutoRefreshInterval = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// CancellationTokenSource for the timer.
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _timerCancellationTokenSource;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
_timerCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RunPeriodicRefreshAsync(_timerCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the service status periodically while waiting for specified amount of ms in between.
|
||||
/// </summary>
|
||||
private async Task RunPeriodicRefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await InitPage();
|
||||
await Task.Delay(AutoRefreshInterval, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_timerCancellationTokenSource?.Cancel();
|
||||
_timerCancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CSS classes for the SMTP button based on its current state.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the CSS classes for the button.</returns>
|
||||
private string GetSmtpButtonClasses()
|
||||
{
|
||||
string buttonClass = "cursor-pointer ";
|
||||
|
||||
if (!IsHeartbeatValid())
|
||||
{
|
||||
buttonClass += "bg-gray-600";
|
||||
}
|
||||
else if (SmtpStatus)
|
||||
{
|
||||
buttonClass += "bg-green-600";
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonClass += "bg-red-600";
|
||||
}
|
||||
|
||||
return buttonClass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip text for the SMTP button.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the tooltip text.</returns>
|
||||
private string GetButtonTooltip()
|
||||
{
|
||||
return IsHeartbeatValid() ? "" : "Heartbeat offline";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the heartbeat is valid (within the last 5 minutes).
|
||||
/// </summary>
|
||||
/// <returns>True if the heartbeat is valid, false otherwise.</returns>
|
||||
private bool IsHeartbeatValid()
|
||||
{
|
||||
return DateTime.Now <= LastHeartbeat.AddMinutes(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the click event for the SMTP button.
|
||||
/// </summary>
|
||||
private async void SmtpClick()
|
||||
{
|
||||
if (!IsHeartbeatValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SmtpPending = true;
|
||||
StateHasChanged();
|
||||
|
||||
SmtpStatus = !SmtpStatus;
|
||||
await UpdateSmtpStatus(SmtpStatus);
|
||||
|
||||
SmtpPending = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the page by fetching service statuses and updating the SMTP status.
|
||||
/// </summary>
|
||||
private async Task InitPage()
|
||||
{
|
||||
if (InitInProgress || SmtpPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InitInProgress = true;
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
ServiceStatus = await dbContext.WorkerServiceStatuses.ToListAsync();
|
||||
|
||||
var smtpEntry = ServiceStatus.Find(x => x.ServiceName == "AliasVault.SmtpService");
|
||||
if (smtpEntry != null)
|
||||
{
|
||||
LastHeartbeat = smtpEntry.Heartbeat;
|
||||
SmtpStatus = IsHeartbeatValid() && smtpEntry.CurrentStatus == "Started";
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
InitInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the service statuses.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateServiceStatus(string serviceName, bool newStatus)
|
||||
{
|
||||
// Refresh the DbContext to ensure we get the latest data.
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var entry = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstOrDefaultAsync();
|
||||
if (entry != null)
|
||||
{
|
||||
string newDesiredStatus = newStatus ? "Started" : "Stopped";
|
||||
entry.DesiredStatus = newDesiredStatus;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Wait for service to have updated its status.
|
||||
var timeout = DateTime.Now.AddSeconds(30);
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.Now > timeout)
|
||||
{
|
||||
// Timeout
|
||||
return false;
|
||||
}
|
||||
|
||||
dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var check = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstAsync();
|
||||
if (check.CurrentStatus == newDesiredStatus)
|
||||
{
|
||||
// Done
|
||||
return true;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the SMTP service status.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateSmtpStatus(bool newStatus)
|
||||
{
|
||||
return await UpdateServiceStatus("AliasVault.SmtpService", newStatus);
|
||||
}
|
||||
}
|
||||
10
src/AliasVault.Admin/Main/Layout/Footer.razor
Normal file
10
src/AliasVault.Admin/Main/Layout/Footer.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using AliasVault.Shared.Core
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 lg:mb-0">
|
||||
© 2024 <span>@AppInfo.ApplicationName v@(AppInfo.GetFullVersion())</span>. All rights reserved.
|
||||
</p>
|
||||
<ul class="flex flex-wrap items-center justify-center">
|
||||
<li><a href="https://github.com/lanedirt/AliasVault/blob/main/LICENSE.md" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">License</a></li>
|
||||
<li><a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
61
src/AliasVault.Admin/Main/Layout/MainLayout.razor
Normal file
61
src/AliasVault.Admin/Main/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,61 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject GlobalLoadingService GlobalLoadingService
|
||||
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<ConfirmModal />
|
||||
<TopMenu />
|
||||
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
<main>
|
||||
<GlobalNotificationDisplay />
|
||||
@Body
|
||||
</main>
|
||||
<Footer></Footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
GlobalLoadingService.OnChange -= OnChange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
GlobalLoadingService.OnChange += OnChange;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnChange()
|
||||
{
|
||||
if (GlobalLoadingService.IsLoading)
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
18
src/AliasVault.Admin/Main/Layout/MainLayout.razor.css
Normal file
18
src/AliasVault.Admin/Main/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,18 @@
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
@inherits PageBase
|
||||
@inherits MainBase
|
||||
@implements IDisposable
|
||||
|
||||
<header>
|
||||
<nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4">
|
||||
<nav class="fixed z-30 w-full border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4 bg-primary-100">
|
||||
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
|
||||
<div class="flex justify-start items-center">
|
||||
<a href="/" class="flex mr-14">
|
||||
<img src="/icon-trimmed.png" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<a href="@NavigationService.BaseUri" class="flex mr-14 flex-shrink-0">
|
||||
<img src="/img/logo.svg" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
|
||||
<span class="ps-2 self-center hidden sm:flex text-sm font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
</a>
|
||||
|
||||
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1">
|
||||
<ul class="flex flex-col mt-4 space-x-6 text-sm font-medium lg:flex-row xl:space-x-8 lg:mt-0">
|
||||
<NavLink href="/" class="block rounded text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Home
|
||||
<NavLink href="users" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Users
|
||||
</NavLink>
|
||||
<NavLink href="/aliases" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Aliases
|
||||
<NavLink href="emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Emails
|
||||
</NavLink>
|
||||
<NavLink href="logging/general" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
General logs
|
||||
</NavLink>
|
||||
<NavLink href="logging/auth" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Auth logs
|
||||
</NavLink>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center lg:order-2">
|
||||
<div class="mr-3 -mb-1 hidden sm:block">
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-center lg:order-2">
|
||||
<Services />
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
@@ -48,12 +52,12 @@
|
||||
</div>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="#" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
|
||||
<a href="account/manage" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
<a href="user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -71,13 +75,28 @@
|
||||
<nav class="bg-white dark:bg-gray-900">
|
||||
<ul id="mobileMenu" class="flex-col mt-0 pt-16 w-full text-sm font-medium lg:hidden">
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="/" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
<NavLink href="./" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Home
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="/aliases" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Aliases
|
||||
<NavLink href="users" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Users
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="emails" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Emails
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="logging/general" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
General logs
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="logging/auth" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Auth logs
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -90,12 +109,31 @@
|
||||
private bool isMobileMenuOpen = false;
|
||||
private string _username { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Close the menu.
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public void CloseMenu()
|
||||
{
|
||||
isMenuOpen = false;
|
||||
isMobileMenuOpen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose method.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationService.LocationChanged -= LocationChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
_username = await GetUsernameAsync();
|
||||
NavigationManager.LocationChanged += LocationChanged;
|
||||
_username = GetUsername();
|
||||
NavigationService.LocationChanged += LocationChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -110,7 +148,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
void LocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
private void LocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
isMenuOpen = false;
|
||||
isMobileMenuOpen = false;
|
||||
@@ -126,17 +164,4 @@
|
||||
{
|
||||
isMobileMenuOpen = !isMobileMenuOpen;
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void CloseMenu()
|
||||
{
|
||||
isMenuOpen = false;
|
||||
isMobileMenuOpen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= LocationChanged;
|
||||
}
|
||||
}
|
||||
54
src/AliasVault.Admin/Main/Models/UserViewModel.cs
Normal file
54
src/AliasVault.Admin/Main/Models/UserViewModel.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UserViewModel.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Admin.Main.Models;
|
||||
|
||||
/// <summary>
|
||||
/// User view model.
|
||||
/// </summary>
|
||||
public class UserViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CreatedAt timestamp.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user name.
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user has two-factor authentication enabled.
|
||||
/// </summary>
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault count.
|
||||
/// </summary>
|
||||
public int VaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email claim count.
|
||||
/// </summary>
|
||||
public int EmailClaimCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total vault storage that this user takes up in kilobytes.
|
||||
/// </summary>
|
||||
public int VaultStorageInKb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last time the vault was updated.
|
||||
/// </summary>
|
||||
public DateTime LastVaultUpdate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@page "/account/manage/change-password"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject ILogger<ChangePassword> Logger
|
||||
|
||||
<LayoutPageTitle>Change password</LayoutPageTitle>
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Change password</h3>
|
||||
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="old-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Old password</label>
|
||||
<InputText type="password" @bind-Value="Input.OldPassword" id="old-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" autocomplete="current-password" aria-required="true" placeholder="Please enter your old password."/>
|
||||
<ValidationMessage For="() => Input.OldPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">New password</label>
|
||||
<InputText type="password" @bind-Value="Input.NewPassword" id="new-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" autocomplete="new-password" aria-required="true" placeholder="Please enter your new password."/>
|
||||
<ValidationMessage For="() => Input.NewPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Confirm password</label>
|
||||
<InputText type="password" @bind-Value="Input.ConfirmPassword" id="confirm-password" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" autocomplete="new-password" aria-required="true" placeholder="Please confirm your new password."/>
|
||||
<ValidationMessage For="() => Input.ConfirmPassword" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<SubmitButton>Update password</SubmitButton>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
var changePasswordResult = await UserManager.ChangePasswordAsync(UserService.User(), Input.OldPassword, Input.NewPassword);
|
||||
var user = UserService.User();
|
||||
user.LastPasswordChanged = DateTime.UtcNow;
|
||||
await UserService.UpdateUserAsync(user);
|
||||
|
||||
// Clear the password fields
|
||||
Input.OldPassword = "";
|
||||
Input.NewPassword = "";
|
||||
Input.ConfirmPassword = "";
|
||||
|
||||
if (!changePasswordResult.Succeeded)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}", true);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("User changed their password successfully.");
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your password has been changed.");
|
||||
|
||||
NavigationService.RedirectToCurrentPage();
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Current password")]
|
||||
public string OldPassword { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "New password")]
|
||||
public string NewPassword { get; set; } = "";
|
||||
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirm new password")]
|
||||
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<h3 class="text-lg font-medium">Recovery codes</h3>
|
||||
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4" role="alert">
|
||||
<p class="font-semibold">
|
||||
Put these codes in a safe place.
|
||||
</p>
|
||||
<p>
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1">
|
||||
@foreach (var recoveryCode in RecoveryCodes)
|
||||
{
|
||||
<div>
|
||||
<code class="block p-2 bg-primary-200 rounded">@recoveryCode</code>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The recovery codes to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string[] RecoveryCodes { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@page "/account/manage/disable-2fa"
|
||||
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject ILogger<Disable2fa> Logger
|
||||
|
||||
<LayoutPageTitle>Disable two-factor authentication (2FA)</LayoutPageTitle>
|
||||
|
||||
<h3 class="text-xl font-bold mb-4">Disable two-factor authentication (2FA)</h3>
|
||||
|
||||
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
|
||||
<p class="font-bold mb-2">
|
||||
This action only disables 2FA.
|
||||
</p>
|
||||
<p>
|
||||
Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a href="account/manage/reset-authenticator" class="text-primary-600 hover:text-primary-800 underline">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form @formname="disable-2fa" @onsubmit="OnSubmitAsync" method="post">
|
||||
<AntiforgeryToken/>
|
||||
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" type="submit">Disable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await UserManager.GetTwoFactorEnabledAsync(UserService.User()))
|
||||
{
|
||||
throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
var disable2FaResult = await UserManager.SetTwoFactorEnabledAsync(UserService.User(), false);
|
||||
if (!disable2FaResult.Succeeded)
|
||||
{
|
||||
await AuthLoggingService.LogAuthEventFailAsync(UserService.User().UserName!, AuthEventType.TwoFactorAuthDisable, AuthFailureReason.Unknown);
|
||||
throw new InvalidOperationException("Unexpected error occurred disabling 2FA.");
|
||||
}
|
||||
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(UserService.User().UserName!, AuthEventType.TwoFactorAuthDisable);
|
||||
Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", UserService.User().Id);
|
||||
|
||||
// Reload current page.
|
||||
NavigationService.RedirectTo("account/manage/2fa");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
@page "/account/manage/enable-authenticator"
|
||||
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Globalization
|
||||
@using System.Text
|
||||
@using System.Text.Encodings.Web
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject UrlEncoder UrlEncoder
|
||||
@inject ILogger<EnableAuthenticator> Logger
|
||||
|
||||
<LayoutPageTitle>Configure authenticator app</LayoutPageTitle>
|
||||
|
||||
@if (RecoveryCodes is not null)
|
||||
{
|
||||
<ShowRecoveryCodes RecoveryCodes="RecoveryCodes.ToArray()"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Configure authenticator app</h3>
|
||||
<div class="space-y-6">
|
||||
<p class="text-gray-700 dark:text-gray-300">To use an authenticator app go through the following steps:</p>
|
||||
<ol class="list-decimal space-y-4">
|
||||
<li>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Download a two-factor authenticator app like Microsoft Authenticator for
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825072" class="text-blue-600 hover:underline dark:text-blue-400">Android</a> and
|
||||
<a href="https://go.microsoft.com/fwlink/?Linkid=825073" class="text-blue-600 hover:underline dark:text-blue-400">iOS</a> or
|
||||
Google Authenticator for
|
||||
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en" class="text-blue-600 hover:underline dark:text-blue-400">Android</a> and
|
||||
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8" class="text-blue-600 hover:underline dark:text-blue-400">iOS</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-gray-700 dark:text-gray-300">Scan the QR Code or enter this key <kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">@SharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
||||
<div id="authenticator-uri" data-url="@AuthenticatorUri" class="mt-4"></div>
|
||||
</li>
|
||||
<li>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
|
||||
with a unique code. Enter the code in the confirmation box below.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<EditForm Model="Input" FormName="send-code" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-4">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label for="code" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Verification Code</label>
|
||||
<InputText @bind-Value="Input.Code" id="code" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" autocomplete="off" placeholder="Please enter the code."/>
|
||||
<ValidationMessage For="() => Input.Code" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<SubmitButton>Verify</SubmitButton>
|
||||
</div>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
</EditForm>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
private string? SharedKey { get; set; }
|
||||
private string? AuthenticatorUri { get; set; }
|
||||
private IEnumerable<string>? RecoveryCodes { get; set; }
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
await LoadSharedKeyAndQrCodeUriAsync(UserService.User());
|
||||
await JsInvokeService.RetryInvokeAsync("generateQrCode", TimeSpan.Zero, 5, "authenticator-uri");
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
// Strip spaces and hyphens
|
||||
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
var is2FaTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
|
||||
UserService.User(), UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2FaTokenValid)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Error: Verification code is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
await UserManager.SetTwoFactorEnabledAsync(UserService.User(), true);
|
||||
await AuthLoggingService.LogAuthEventSuccessAsync(UserService.User().UserName!, AuthEventType.TwoFactorAuthEnable);
|
||||
Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", UserService.User().Id);
|
||||
GlobalNotificationService.AddSuccessMessage("Your authenticator app has been verified.");
|
||||
|
||||
if (await UserManager.CountRecoveryCodesAsync(UserService.User()) == 0)
|
||||
{
|
||||
RecoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Navigate back to the two factor authentication page.
|
||||
NavigationService.RedirectTo("account/manage/2fa", forceLoad: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask LoadSharedKeyAndQrCodeUriAsync(AdminUser user)
|
||||
{
|
||||
// Load the authenticator key & QR code URI to display on the form
|
||||
var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await UserManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
SharedKey = FormatKey(unformattedKey!);
|
||||
|
||||
var username = await UserManager.GetUserNameAsync(user);
|
||||
AuthenticatorUri = GenerateQrCodeUri(username!, unformattedKey!);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
|
||||
currentPosition += 4;
|
||||
}
|
||||
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.AsSpan(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string username, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
AuthenticatorUriFormat,
|
||||
UrlEncoder.Encode("AliasVault Admin"),
|
||||
UrlEncoder.Encode(username),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
||||
[DataType(DataType.Text)]
|
||||
[Display(Name = "Verification Code")]
|
||||
public string Code { get; set; } = "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@page "/account/manage/generate-recovery-codes"
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject ILogger<GenerateRecoveryCodes> Logger
|
||||
|
||||
<LayoutPageTitle>Generate two-factor authentication (2FA) recovery codes</LayoutPageTitle>
|
||||
|
||||
@if (recoveryCodes is not null)
|
||||
{
|
||||
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3 class="text-xl font-bold mb-4">Generate two-factor authentication (2FA) recovery codes</h3>
|
||||
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
|
||||
<p class="mb-2">
|
||||
<svg class="inline w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<strong>Put these codes in a safe place.</strong>
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
If you lose your device and don't have the recovery codes you will lose access to your account.
|
||||
</p>
|
||||
<p>
|
||||
Generating new recovery codes does not change the keys used in authenticator apps. If you wish to change the key
|
||||
used in an authenticator app you should <a href="account/manage/reset-authenticator" class="text-primary-600 hover:text-primary-800 underline">reset your authenticator keys.</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="bg-primary-600 hover:bg-primary-700 text-white font-bold py-2 px-4 rounded" @onclick="GenerateCodes" type="submit">Generate Recovery Codes</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private IEnumerable<string>? recoveryCodes;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(UserService.User());
|
||||
if (!isTwoFactorEnabled)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateCodes()
|
||||
{
|
||||
var userId = await UserManager.GetUserIdAsync(UserService.User());
|
||||
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
|
||||
GlobalNotificationService.AddSuccessMessage("You have generated new recovery codes.");
|
||||
|
||||
Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
|
||||
}
|
||||
|
||||
}
|
||||
67
src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor
Normal file
67
src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor
Normal file
@@ -0,0 +1,67 @@
|
||||
@page "/account/manage"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
|
||||
<LayoutPageTitle>Profile</LayoutPageTitle>
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Profile</h3>
|
||||
|
||||
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="username" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Username</label>
|
||||
<input type="text" value="@username" id="username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-gray-100 cursor-not-allowed dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400" placeholder="Please choose your username." disabled/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone-number" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Phone number</label>
|
||||
<InputText @bind-Value="Input.PhoneNumber" id="phone-number" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Please enter your phone number."/>
|
||||
<ValidationMessage For="() => Input.PhoneNumber" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? username;
|
||||
private string? phoneNumber;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
username = await UserManager.GetUserNameAsync(UserService.User());
|
||||
phoneNumber = await UserManager.GetPhoneNumberAsync(UserService.User());
|
||||
|
||||
Input.PhoneNumber ??= phoneNumber;
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
if (Input.PhoneNumber != phoneNumber)
|
||||
{
|
||||
var setPhoneResult = await UserManager.SetPhoneNumberAsync(UserService.User(), Input.PhoneNumber);
|
||||
if (!setPhoneResult.Succeeded)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Phone number could not be set", true);
|
||||
}
|
||||
}
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your profile has been updated", true);
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Phone]
|
||||
[Display(Name = "Phone number")]
|
||||
public string? PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@page "/account/manage/reset-authenticator"
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject ILogger<ResetAuthenticator> Logger
|
||||
|
||||
<LayoutPageTitle>Reset authenticator key</LayoutPageTitle>
|
||||
|
||||
<h3 class="text-xl font-bold mb-4 dark:text-white">Reset authenticator key</h3>
|
||||
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4" role="alert">
|
||||
<p class="mb-2">
|
||||
<svg class="inline w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
|
||||
</p>
|
||||
<p>
|
||||
This process disables 2FA until you verify your authenticator app.
|
||||
If you do not complete your authenticator app configuration you may lose access to your account.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<form @formname="reset-authenticator" @onsubmit="OnSubmitAsync" method="post">
|
||||
<AntiforgeryToken/>
|
||||
<SubmitButton>Reset authenticator key</SubmitButton>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private async Task OnSubmitAsync()
|
||||
{
|
||||
await UserManager.SetTwoFactorEnabledAsync(UserService.User(), false);
|
||||
await UserManager.ResetAuthenticatorKeyAsync(UserService.User());
|
||||
var userId = await UserManager.GetUserIdAsync(UserService.User());
|
||||
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your authenticator app key has been reset, you will need to re-configure your authenticator app using the new key.");
|
||||
|
||||
NavigationService.RedirectTo(
|
||||
"account/manage/2fa");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
@page "/account/manage/2fa"
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
|
||||
<LayoutPageTitle>Two-factor authentication (2FA)</LayoutPageTitle>
|
||||
|
||||
@if (is2FaEnabled)
|
||||
{
|
||||
<div class="mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Two-factor authentication (2FA)</h3>
|
||||
|
||||
@if (recoveryCodesLeft == 0)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
|
||||
<p class="font-bold">You have no recovery codes left.</p>
|
||||
<p>You must <a href="account/manage/generate-recovery-codes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
</div>
|
||||
}
|
||||
else if (recoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
|
||||
<p class="font-bold">You have 1 recovery code left.</p>
|
||||
<p>You can <a href="account/manage/generate-recovery-codes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (recoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-100">
|
||||
<p class="font-bold">You have @recoveryCodesLeft recovery codes left.</p>
|
||||
<p>You should <a href="account/manage/generate-recovery-codes" class="text-yellow-800 dark:text-yellow-200 underline">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<a href="account/manage/disable-2fa" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Disable 2FA</a>
|
||||
<a href="account/manage/generate-recovery-codes" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg text-sm focus:ring-4 focus:outline-none focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset recovery codes</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Authenticator app</h4>
|
||||
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
|
||||
@if (!hasAuthenticator)
|
||||
{
|
||||
<LinkButton Href="account/manage/enable-authenticator" Color="primary" Text="Add authenticator app" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<LinkButton Href="account/manage/enable-authenticator" Color="primary" Text="Add authenticator app" />
|
||||
<LinkButton Href="account/manage/reset-authenticator" Color="primary" Text="Reset authenticator app" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool hasAuthenticator;
|
||||
private int recoveryCodesLeft;
|
||||
private bool is2FaEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(UserService.User()) is not null;
|
||||
is2FaEnabled = await UserManager.GetTwoFactorEnabledAsync(UserService.User());
|
||||
recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(UserService.User());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@layout ManageLayout
|
||||
@inherits MainBase
|
||||
@using AliasVault.Admin.Auth
|
||||
@using AliasVault.Admin.Main.Pages.Account.Manage.Components
|
||||
@using AliasVault.Admin.Main.Components.Layout
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
37
src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor
Normal file
37
src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor
Normal file
@@ -0,0 +1,37 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using AliasVault.Admin.Main.Layout
|
||||
@layout MainLayout
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Manage account"
|
||||
Description="Manage your profile here.">
|
||||
</PageHeader>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<hr class="mb-6 border-t border-gray-300"/>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<div class="w-full md:w-1/4 mb-6 md:mb-0">
|
||||
<ManageNavMenu/>
|
||||
</div>
|
||||
<div class="w-full md:w-3/4 md:pl-8">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
private List<BreadcrumbItem> BreadcrumbItems { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Add base breadcrumbs.
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = "/" });
|
||||
}
|
||||
}
|
||||
15
src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor
Normal file
15
src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
|
||||
<ul class="flex flex-col space-y-1">
|
||||
<li>
|
||||
<NavLink href="account/manage" Match="NavLinkMatch.All" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Profile</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="account/manage/change-password" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Password</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="account/manage/2fa" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Two-factor authentication</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
147
src/AliasVault.Admin/Main/Pages/Emails.razor
Normal file
147
src/AliasVault.Admin/Main/Pages/Emails.razor
Normal file
@@ -0,0 +1,147 @@
|
||||
@page "/emails"
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Emails</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Emails"
|
||||
Description="This page gives an overview of recently received mails by this AliasVault server. Note that all email fields except 'To' are encrypted with the public key of the user and cannot be decrypted by the server.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="overflow-x-auto px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var email in EmailList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@email.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@email.DateSystem.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@(email.FromLocal.Length > 15 ? email.FromLocal.Substring(0, 15) : email.FromLocal)@@@(email.FromDomain.Length > 15 ? email.FromDomain.Substring(0, 15) : email.FromDomain)</SortableTableColumn>
|
||||
<SortableTableColumn>@email.ToLocal@@@email.ToDomain</SortableTableColumn>
|
||||
<SortableTableColumn>@(email.Subject.Length > 30 ? email.Subject.Substring(0, 30) : email.Subject)</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
<span class="line-clamp-1">
|
||||
@(email.MessagePreview?.Length > 30 ? email.MessagePreview.Substring(0, 30) : email.MessagePreview)
|
||||
</span>
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn>@email.Attachments.Count</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<TableColumn> _tableColumns = [
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Time", PropertyName = "DateSystem" },
|
||||
new TableColumn { Title = "From", PropertyName = "From" },
|
||||
new TableColumn { Title = "To", PropertyName = "To" },
|
||||
new TableColumn { Title = "Subject", PropertyName = "Subject" },
|
||||
new TableColumn { Title = "Preview", PropertyName = "MessagePreview" },
|
||||
new TableColumn { Title = "Attachments", PropertyName = "Attachments" },
|
||||
];
|
||||
|
||||
private List<Email> EmailList { get; set; } = [];
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
|
||||
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
IQueryable<Email> query = DbContext.Emails;
|
||||
|
||||
// Apply sort
|
||||
switch (SortColumn)
|
||||
{
|
||||
case "Id":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Id)
|
||||
: query.OrderByDescending(x => x.Id);
|
||||
break;
|
||||
case "DateSystem":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.DateSystem)
|
||||
: query.OrderByDescending(x => x.DateSystem);
|
||||
break;
|
||||
case "From":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.FromLocal + "@" + x.FromDomain)
|
||||
: query.OrderByDescending(x => x.FromLocal + "@" + x.FromDomain);
|
||||
break;
|
||||
case "To":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.ToLocal + "@" + x.ToDomain)
|
||||
: query.OrderByDescending(x => x.ToLocal + "@" + x.ToDomain);
|
||||
break;
|
||||
case "Subject":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Subject)
|
||||
: query.OrderByDescending(x => x.Subject);
|
||||
break;
|
||||
case "MessagePreview":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.MessagePreview)
|
||||
: query.OrderByDescending(x => x.MessagePreview);
|
||||
break;
|
||||
case "Attachments":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Attachments.Count)
|
||||
: query.OrderByDescending(x => x.Attachments.Count);
|
||||
break;
|
||||
default:
|
||||
query = query.OrderByDescending(x => x.DateSystem);
|
||||
break;
|
||||
}
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
EmailList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
@inherits AuthorizePageBase
|
||||
@page "/Error"
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
<LayoutPageTitle>Error</LayoutPageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
@@ -31,6 +30,7 @@
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
|
||||
21
src/AliasVault.Admin/Main/Pages/Home.razor
Normal file
21
src/AliasVault.Admin/Main/Pages/Home.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@page "/"
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Home</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="AliasVault Admin"
|
||||
Description="Welcome to the AliasVault admin portal.">
|
||||
</PageHeader>
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
// Redirect to users page.
|
||||
NavigationService.RedirectTo("users");
|
||||
}
|
||||
}
|
||||
225
src/AliasVault.Admin/Main/Pages/Logging/Auth.razor
Normal file
225
src/AliasVault.Admin/Main/Pages/Logging/Auth.razor
Normal file
@@ -0,0 +1,225 @@
|
||||
@page "/logging/auth"
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Auth logs</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Auth logs"
|
||||
Description="This page gives an overview of recent auth attempts.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<div class="mb-4 flex space-x-4">
|
||||
<div class="flex w-full">
|
||||
<div class="w-2/3 pr-2">
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
<div class="w-1/3 pl-2">
|
||||
<select @bind="SelectedEventType" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
<option value="">All event types</option>
|
||||
@foreach (var eventType in Enum.GetValues<AuthEventType>())
|
||||
{
|
||||
<option value="@eventType">@eventType</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var log in LogList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@log.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@log.Timestamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@log.Username</SortableTableColumn>
|
||||
<SortableTableColumn>@log.EventType</SortableTableColumn>
|
||||
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="Failed" /></SortableTableColumn>
|
||||
<SortableTableColumn>@log.IpAddress</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<TableColumn> _tableColumns = [
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
|
||||
new TableColumn { Title = "Username", PropertyName = "Username" },
|
||||
new TableColumn { Title = "Event", PropertyName = "EventType" },
|
||||
new TableColumn { Title = "Success", PropertyName = "IsSuccess" },
|
||||
new TableColumn { Title = "IP", PropertyName = "IpAddress" },
|
||||
];
|
||||
|
||||
private List<AuthLog> LogList { get; set; } = [];
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
set
|
||||
{
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _selectedEventType = string.Empty;
|
||||
private string SelectedEventType
|
||||
{
|
||||
get => _selectedEventType;
|
||||
set
|
||||
{
|
||||
if (_selectedEventType != value)
|
||||
{
|
||||
_selectedEventType = value;
|
||||
_ = RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
|
||||
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var query = DbContext.AuthLogs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
query = query.Where(x => EF.Functions.Like(x.Username.ToLower(), "%" + SearchTerm.ToLower() + "%"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedEventType))
|
||||
{
|
||||
var success = Enum.TryParse<AuthEventType>(SelectedEventType, out var eventType);
|
||||
if (success)
|
||||
{
|
||||
query = query.Where(x => x.EventType == eventType);
|
||||
}
|
||||
}
|
||||
|
||||
query = ApplySort(query);
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply sort to the query.
|
||||
/// </summary>
|
||||
private IQueryable<AuthLog> ApplySort(IQueryable<AuthLog> query)
|
||||
{
|
||||
// Apply sort.
|
||||
switch (SortColumn)
|
||||
{
|
||||
case "Timestamp":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Timestamp)
|
||||
: query.OrderByDescending(x => x.Timestamp);
|
||||
break;
|
||||
case "Username":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Username)
|
||||
: query.OrderByDescending(x => x.Username);
|
||||
break;
|
||||
case "EventType":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.EventType)
|
||||
: query.OrderByDescending(x => x.EventType);
|
||||
break;
|
||||
case "IsSuccess":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.IsSuccess)
|
||||
: query.OrderByDescending(x => x.IsSuccess);
|
||||
break;
|
||||
case "IpAddress":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.IpAddress)
|
||||
: query.OrderByDescending(x => x.IpAddress);
|
||||
break;
|
||||
default:
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Id)
|
||||
: query.OrderByDescending(x => x.Id);
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private async Task DeleteLogsWithConfirmation()
|
||||
{
|
||||
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
|
||||
{
|
||||
await DeleteLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLogs()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
DbContext.AuthLogs.RemoveRange(DbContext.AuthLogs);
|
||||
await DbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
228
src/AliasVault.Admin/Main/Pages/Logging/General.razor
Normal file
228
src/AliasVault.Admin/Main/Pages/Logging/General.razor
Normal file
@@ -0,0 +1,228 @@
|
||||
@page "/logging/general"
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>System logs</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="General logs"
|
||||
Description="This page gives an overview of recent system logs.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<div class="mb-4 flex space-x-4">
|
||||
<div class="flex w-full">
|
||||
<div class="w-2/3 pr-2">
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
<div class="w-1/3 pl-2">
|
||||
<select @bind="SelectedServiceName" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
<option value="">All Services</option>
|
||||
@foreach (var service in ServiceNames)
|
||||
{
|
||||
<option value="@service">@service</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var log in LogList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@log.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@log.TimeStamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@log.Application</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
@{
|
||||
string bgColor = log.Level switch
|
||||
{
|
||||
"Information" => "bg-blue-500",
|
||||
"Error" => "bg-red-500",
|
||||
"Warning" => "bg-yellow-500",
|
||||
"Debug" => "bg-green-500",
|
||||
_ => "bg-gray-500"
|
||||
};
|
||||
}
|
||||
<span class="px-2 py-1 rounded-full text-white @bgColor">
|
||||
@log.Level
|
||||
</span>
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn Title="@log.Exception">
|
||||
@if (log.SourceContext.Length > 0)
|
||||
{
|
||||
<span>@log.SourceContext: </span>
|
||||
}
|
||||
@log.Message
|
||||
</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<TableColumn> _tableColumns = [
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
|
||||
new TableColumn { Title = "Application", PropertyName = "Application" },
|
||||
new TableColumn { Title = "Level", PropertyName = "Level" },
|
||||
new TableColumn { Title = "Message", PropertyName = "Message" },
|
||||
];
|
||||
|
||||
private List<Log> LogList { get; set; } = [];
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
set
|
||||
{
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _selectedServiceName = string.Empty;
|
||||
private string SelectedServiceName
|
||||
{
|
||||
get => _selectedServiceName;
|
||||
set
|
||||
{
|
||||
if (_selectedServiceName != value)
|
||||
{
|
||||
_selectedServiceName = value;
|
||||
_ = RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> ServiceNames { get; set; } = [];
|
||||
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
|
||||
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
ServiceNames = await DbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var query = DbContext.Logs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
query = query.Where(x => EF.Functions.Like(x.Application.ToLower(), "%" + SearchTerm.ToLower() + "%") ||
|
||||
EF.Functions.Like(x.Message.ToLower(), "%" + SearchTerm.ToLower() + "%") ||
|
||||
EF.Functions.Like(x.Level.ToLower(), "%" + SearchTerm.ToLower() + "%"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedServiceName))
|
||||
{
|
||||
query = query.Where(x => x.Application == SelectedServiceName);
|
||||
}
|
||||
|
||||
// Apply sort.
|
||||
switch (SortColumn)
|
||||
{
|
||||
case "Application":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Application)
|
||||
: query.OrderByDescending(x => x.Application);
|
||||
break;
|
||||
case "Message":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Message)
|
||||
: query.OrderByDescending(x => x.Message);
|
||||
break;
|
||||
case "Level":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Level)
|
||||
: query.OrderByDescending(x => x.Level);
|
||||
break;
|
||||
case "Timestamp":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.TimeStamp)
|
||||
: query.OrderByDescending(x => x.TimeStamp);
|
||||
break;
|
||||
default:
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Id)
|
||||
: query.OrderByDescending(x => x.Id);
|
||||
break;
|
||||
}
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteLogsWithConfirmation()
|
||||
{
|
||||
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
|
||||
{
|
||||
await DeleteLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteLogs()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
DbContext.Logs.RemoveRange(DbContext.Logs);
|
||||
await DbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user