Compare commits
677 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fdcee50d5 | ||
|
|
8526172ec7 | ||
|
|
5156988319 | ||
|
|
18d92ecced | ||
|
|
0a0bec99b1 | ||
|
|
791f8a758b | ||
|
|
3f11e29787 | ||
|
|
046d09453a | ||
|
|
1d77d05e7c | ||
|
|
22d2e09982 | ||
|
|
8b835a4a77 | ||
|
|
a435305093 | ||
|
|
e4f3de927f | ||
|
|
1d5c288514 | ||
|
|
5d3ad60dee | ||
|
|
c5244b31ec | ||
|
|
a6c7c54592 | ||
|
|
bf46c155bd | ||
|
|
d4e5b724ff | ||
|
|
e51219d513 | ||
|
|
800f015947 | ||
|
|
5f3c36263d | ||
|
|
4617d5efc4 | ||
|
|
1401982e2c | ||
|
|
ebdbf41208 | ||
|
|
ed4b82e125 | ||
|
|
1976255e98 | ||
|
|
e817326162 | ||
|
|
9d2a397317 | ||
|
|
8f42ebdfa4 | ||
|
|
3aab43b17a | ||
|
|
6e922237c0 | ||
|
|
ebac252162 | ||
|
|
9df76ffb43 | ||
|
|
2d59117112 | ||
|
|
ccb66af1ca | ||
|
|
f4093a9199 | ||
|
|
290601ccfb | ||
|
|
77be2a339e | ||
|
|
c0b23c15e7 | ||
|
|
4af158b35d | ||
|
|
abfabc2a4a | ||
|
|
a0036da781 | ||
|
|
99f084558d | ||
|
|
d7be5fc308 | ||
|
|
485e867c50 | ||
|
|
d2e5f3c715 | ||
|
|
0cbe5fec93 | ||
|
|
7f7c729e82 | ||
|
|
35cc29e751 | ||
|
|
8a16a29727 | ||
|
|
708cffc49e | ||
|
|
74c0ace2b5 | ||
|
|
55175a7db6 | ||
|
|
7e1f33e4e1 | ||
|
|
81362b165b | ||
|
|
41d6511eb2 | ||
|
|
60ba96cb86 | ||
|
|
fdd8c8b37e | ||
|
|
53fcb2f2e4 | ||
|
|
b1848320d9 | ||
|
|
610be7e30b | ||
|
|
933e458776 | ||
|
|
b460e6ec20 | ||
|
|
80cd371ee3 | ||
|
|
915e12d541 | ||
|
|
c8d78e0b02 | ||
|
|
199941a837 | ||
|
|
1e0c586dba | ||
|
|
37e59dcd4e | ||
|
|
e665130ea7 | ||
|
|
c0aac4ef72 | ||
|
|
8319ddcce4 | ||
|
|
adc6293f4b | ||
|
|
418bfed663 | ||
|
|
7074113cbf | ||
|
|
ddb610051a | ||
|
|
188b7a4062 | ||
|
|
989d17708f | ||
|
|
77a4b4fcba | ||
|
|
0462e3522b | ||
|
|
f6bddf730f | ||
|
|
035403e3e3 | ||
|
|
33ebbf0fd5 | ||
|
|
55c75ec094 | ||
|
|
6e244e611c | ||
|
|
e1dc9eb447 | ||
|
|
7a8b31a98a | ||
|
|
9baa70f022 | ||
|
|
24106475f9 | ||
|
|
c50178967a | ||
|
|
a69a6a91e2 | ||
|
|
1dca845731 | ||
|
|
9bec5a3ae5 | ||
|
|
1a8dae44ec | ||
|
|
ec15c76001 | ||
|
|
e0c11ba0f6 | ||
|
|
a72f1139f9 | ||
|
|
a3a3d39664 | ||
|
|
014a705a5e | ||
|
|
6dfb922292 | ||
|
|
cb78d8a636 | ||
|
|
a4c4a9c8ec | ||
|
|
6f5ae7c17e | ||
|
|
43f5e0c647 | ||
|
|
0e5f611670 | ||
|
|
70b7ac6f9f | ||
|
|
14ee466bec | ||
|
|
ea9c3c5683 | ||
|
|
30b812e8a3 | ||
|
|
27ba14ee34 | ||
|
|
2e851701f9 | ||
|
|
a2c2caed79 | ||
|
|
c00e6c6a4d | ||
|
|
09dda0147b | ||
|
|
ef7398b47a | ||
|
|
dc769bb5d4 | ||
|
|
634fc281a2 | ||
|
|
e93b0575ff | ||
|
|
3f6575dfe5 | ||
|
|
390877f8f3 | ||
|
|
55ee3bfd4a | ||
|
|
423fe00692 | ||
|
|
f8e0d6a293 | ||
|
|
a20b0ed83a | ||
|
|
ca043954ec | ||
|
|
4f0104e8f9 | ||
|
|
ea37c4d8c6 | ||
|
|
95be4beb13 | ||
|
|
716ef0b30c | ||
|
|
fc0eb0e7e7 | ||
|
|
9670178aec | ||
|
|
8503be4d52 | ||
|
|
9eadcaa2ed | ||
|
|
e0ed8fd285 | ||
|
|
61748c3d03 | ||
|
|
faff4844f5 | ||
|
|
09d931484a | ||
|
|
1678595c13 | ||
|
|
8945b33705 | ||
|
|
4ee044ffb9 | ||
|
|
5443e147b1 | ||
|
|
05edda8b48 | ||
|
|
179bb62604 | ||
|
|
1f5863b066 | ||
|
|
ef36a08ef4 | ||
|
|
4f7212668e | ||
|
|
41bb7ed701 | ||
|
|
78286b1ac1 | ||
|
|
7bc8bb3fc2 | ||
|
|
c576062025 | ||
|
|
1194d54e6f | ||
|
|
e782a6a51f | ||
|
|
2071a7c4fe | ||
|
|
8c1e5a7bf8 | ||
|
|
b8f9e7fa2c | ||
|
|
a0a541aff9 | ||
|
|
d6932f33ea | ||
|
|
9ea845b497 | ||
|
|
917d6f6bcc | ||
|
|
39a263d157 | ||
|
|
c7360ee23c | ||
|
|
d1924f4044 | ||
|
|
4d86356990 | ||
|
|
505a2445eb | ||
|
|
75385c4b5d | ||
|
|
4d4053c7fb | ||
|
|
43062d0d93 | ||
|
|
956709da54 | ||
|
|
496e0ab754 | ||
|
|
ef97aac848 | ||
|
|
998fa1913f | ||
|
|
79cd265c3e | ||
|
|
ed5fd5b861 | ||
|
|
5e2dde252d | ||
|
|
79950ab9fc | ||
|
|
dffa651512 | ||
|
|
2dc36cea11 | ||
|
|
ad4c2c7b41 | ||
|
|
2022cdb58b | ||
|
|
5f779ce360 | ||
|
|
b9d981f80b | ||
|
|
65110abf4c | ||
|
|
b0e939ef23 | ||
|
|
607c0da5b4 | ||
|
|
1de7f831b5 | ||
|
|
ef328718cd | ||
|
|
465c4cc730 | ||
|
|
0dceeeffa4 | ||
|
|
af24464a8d | ||
|
|
5aa82d8149 | ||
|
|
e848e05cce | ||
|
|
323be10d03 | ||
|
|
51b382a739 | ||
|
|
7954104dfc | ||
|
|
4c7b44c04a | ||
|
|
b41449f892 | ||
|
|
934d0d9e56 | ||
|
|
99d0da1119 | ||
|
|
c74e05d400 | ||
|
|
844bdab92f | ||
|
|
1345e3c657 | ||
|
|
4fdf7ce92c | ||
|
|
852d9b5e98 | ||
|
|
3c72fa3fde | ||
|
|
b61b747e4b | ||
|
|
1b4389c7d7 | ||
|
|
499d2759ce | ||
|
|
d0140a8ddb | ||
|
|
76dc465032 | ||
|
|
84420104ee | ||
|
|
1109bde521 | ||
|
|
134a173148 | ||
|
|
83be492b3a | ||
|
|
fac72e5a11 | ||
|
|
5eb885da20 | ||
|
|
da4f286757 | ||
|
|
f6db447ad4 | ||
|
|
b472ba749c | ||
|
|
ef68b3b265 | ||
|
|
08d4a8b656 | ||
|
|
93ac131508 | ||
|
|
a7d1536140 | ||
|
|
4fa3fedea2 | ||
|
|
038e8babb1 | ||
|
|
0845477041 | ||
|
|
90156dd1f8 | ||
|
|
fe4b11cf4d | ||
|
|
2cbf234d05 | ||
|
|
a53575b4bf | ||
|
|
697abc6828 | ||
|
|
e96cfa3940 | ||
|
|
61a88e6715 | ||
|
|
e07a35b214 | ||
|
|
4a79fafbb9 | ||
|
|
02b9bff64e | ||
|
|
55e02478b4 | ||
|
|
a576908ae2 | ||
|
|
95510f793b | ||
|
|
20a4a82b1b | ||
|
|
61ba6e1a3c | ||
|
|
f28f1f07b8 | ||
|
|
7f186f1345 | ||
|
|
129b50afba | ||
|
|
bad0f485a9 | ||
|
|
5d9ae7d189 | ||
|
|
ef8ab63b66 | ||
|
|
469466995c | ||
|
|
62c5edc7dc | ||
|
|
ba625a30ea | ||
|
|
bcdcbef912 | ||
|
|
a64ed4817a | ||
|
|
919a33defb | ||
|
|
7e08f64175 | ||
|
|
e525bd1c2d | ||
|
|
7298f8914d | ||
|
|
c476c53101 | ||
|
|
b6c7e88000 | ||
|
|
26624e165a | ||
|
|
c079b830b5 | ||
|
|
165a89e946 | ||
|
|
5042e1b696 | ||
|
|
472a79a12b | ||
|
|
97730cd721 | ||
|
|
d5400faf95 | ||
|
|
9b8da64858 | ||
|
|
9ce776be2b | ||
|
|
d674c77216 | ||
|
|
e41c4b3213 | ||
|
|
f88670787f | ||
|
|
261be3ab34 | ||
|
|
0bace49e95 | ||
|
|
bb82952c74 | ||
|
|
fd5244a686 | ||
|
|
09bc4286d9 | ||
|
|
4c45047d23 | ||
|
|
5251ea53ca | ||
|
|
2da9955213 | ||
|
|
fab12daacf | ||
|
|
9ba467479a | ||
|
|
8e698a21fa | ||
|
|
28a0c7eb1f | ||
|
|
fcbe8da1e6 | ||
|
|
a0a3a2e14a | ||
|
|
4fff14480b | ||
|
|
c7ad42a63e | ||
|
|
6df3c03682 | ||
|
|
7da5557b98 | ||
|
|
38399e00cb | ||
|
|
b30338de37 | ||
|
|
ceaa7731fe | ||
|
|
b66c41e4c9 | ||
|
|
9e478c94f9 | ||
|
|
b415043b4e | ||
|
|
10f6525e94 | ||
|
|
5fb12f26fe | ||
|
|
6047c8f80d | ||
|
|
1b6e220c5a | ||
|
|
b2093b5892 | ||
|
|
b81eabc583 | ||
|
|
0c4be1398d | ||
|
|
4aa0e5f8a1 | ||
|
|
63c737b6cc | ||
|
|
44c2331b42 | ||
|
|
8f9058e1b8 | ||
|
|
613fb7db12 | ||
|
|
c4738637f1 | ||
|
|
151cb19de8 | ||
|
|
b0c53ca7b4 | ||
|
|
586285c5e8 | ||
|
|
5ca8fb92c8 | ||
|
|
206254574a | ||
|
|
9a9fb12d73 | ||
|
|
5d0540ee2b | ||
|
|
59726d87e8 | ||
|
|
7dccb6443a | ||
|
|
451fe98102 | ||
|
|
a82b7d7ce5 | ||
|
|
9cbaf51778 | ||
|
|
1847293162 | ||
|
|
e5a174443d | ||
|
|
2382ee6592 | ||
|
|
7253d1fee2 | ||
|
|
bc16167293 | ||
|
|
eb587e3496 | ||
|
|
6d0352923a | ||
|
|
6d33f99d62 | ||
|
|
9fbdb2efbb | ||
|
|
50817b65d3 | ||
|
|
5750eef248 | ||
|
|
5cd5efca4a | ||
|
|
7ce841b4b5 | ||
|
|
5e1c79610f | ||
|
|
a2ccee984b | ||
|
|
f9977fb29e | ||
|
|
f8ea8fc7ce | ||
|
|
4ab5be17c0 | ||
|
|
ad8f13928e | ||
|
|
29af7c2196 | ||
|
|
b25f6580cd | ||
|
|
71ae5d0904 | ||
|
|
5baede08a7 | ||
|
|
34995fe801 | ||
|
|
92a2511d9d | ||
|
|
41486c940c | ||
|
|
47c77ade02 | ||
|
|
a51621970d | ||
|
|
39f339b659 | ||
|
|
65d1ca1564 | ||
|
|
5c010cd873 | ||
|
|
88ba57ce88 | ||
|
|
4d266beb0d | ||
|
|
536688d110 | ||
|
|
e343b48fe7 | ||
|
|
9d02737516 | ||
|
|
4a1583a7ff | ||
|
|
4f8125ddb0 | ||
|
|
972505c174 | ||
|
|
d5e83d2319 | ||
|
|
9daac83768 | ||
|
|
bb477e6f91 | ||
|
|
b216a9d2a9 | ||
|
|
b66bcefdde | ||
|
|
6fdb9a7c3e | ||
|
|
411b2262e1 | ||
|
|
72b82671f9 | ||
|
|
713c21b60c | ||
|
|
84b592df7b | ||
|
|
8420f2d42e | ||
|
|
58ed0bbf4a | ||
|
|
153e10fcd0 | ||
|
|
717dec329b | ||
|
|
0bd25e70f5 | ||
|
|
920c1bdebe | ||
|
|
6506b57d9f | ||
|
|
aea98a51a9 | ||
|
|
7f28001f7e | ||
|
|
d1d6bfb957 | ||
|
|
a3b1fc0a21 | ||
|
|
38ac1f731a | ||
|
|
c7d0013b9f | ||
|
|
1634721474 | ||
|
|
f227725778 | ||
|
|
912bdfbe7d | ||
|
|
c82e9a9517 | ||
|
|
d765978e63 | ||
|
|
3d819dce2a | ||
|
|
23bbc7eedb | ||
|
|
26b9d07e7c | ||
|
|
89a24ead1a | ||
|
|
10e2787b4f | ||
|
|
d93a6c603d | ||
|
|
7bc1ccdb7b | ||
|
|
f30b3895ba | ||
|
|
ef8c61c6c9 | ||
|
|
7c65247162 | ||
|
|
af166c27fd | ||
|
|
90b1d0ae09 | ||
|
|
b4c84d9894 | ||
|
|
bce4327f2d | ||
|
|
1fe967624f | ||
|
|
1ee02a3d22 | ||
|
|
ac7b6facd6 | ||
|
|
58e294b509 | ||
|
|
e8314f91dc | ||
|
|
977acf84c5 | ||
|
|
aa9619efad | ||
|
|
e6ccea1c59 | ||
|
|
f691056db6 | ||
|
|
08d7013f75 | ||
|
|
067a949c49 | ||
|
|
38ee886be2 | ||
|
|
9ae5e994bd | ||
|
|
42573bf1fc | ||
|
|
59e99153c3 | ||
|
|
d2c24792fe | ||
|
|
d674563275 | ||
|
|
e153dc6d2a | ||
|
|
fdbf3db6bb | ||
|
|
a6529d67fa | ||
|
|
45f748e247 | ||
|
|
57673b5ee0 | ||
|
|
8ea0273174 | ||
|
|
a31c516fa5 | ||
|
|
bb9e986874 | ||
|
|
533065c7d3 | ||
|
|
16a22b6fa3 | ||
|
|
4d42e7b32e | ||
|
|
b50205b318 | ||
|
|
196e19573d | ||
|
|
9de7f81053 | ||
|
|
75cf43aaba | ||
|
|
1d76597ee2 | ||
|
|
887e91f4c6 | ||
|
|
c4afb9eeb2 | ||
|
|
9151e504bc | ||
|
|
b20d330fdc | ||
|
|
9c5f5fa5cd | ||
|
|
de85430998 | ||
|
|
6df6bb071a | ||
|
|
1263639ca2 | ||
|
|
ad52ec5db1 | ||
|
|
665abcd894 | ||
|
|
8b5cd28e4d | ||
|
|
45a5d7fb20 | ||
|
|
7fefe9f0bb | ||
|
|
3fe5fbd981 | ||
|
|
c13e0571ab | ||
|
|
fbf7f5b4e4 | ||
|
|
8c132f30fb | ||
|
|
7def472df5 | ||
|
|
0069b8cfc6 | ||
|
|
53246a3d99 | ||
|
|
11a33d5ea7 | ||
|
|
f5fb69e756 | ||
|
|
519bd0801d | ||
|
|
cba4a6d3ec | ||
|
|
5daa95a876 | ||
|
|
b81613b785 | ||
|
|
2013f48ddd | ||
|
|
867b37ab79 | ||
|
|
390c77448e | ||
|
|
7f23c4820c | ||
|
|
d6c3bd5cc1 | ||
|
|
ccdb62762e | ||
|
|
e5552e80e6 | ||
|
|
47201b5433 | ||
|
|
0862aa64cb | ||
|
|
bd833414ad | ||
|
|
a3d8242dc4 | ||
|
|
6ea4a9724f | ||
|
|
84d3a25304 | ||
|
|
a9044e95ca | ||
|
|
9eaf8fb369 | ||
|
|
c2d035510a | ||
|
|
117da9dfc8 | ||
|
|
0633bc2943 | ||
|
|
e153dc6fe7 | ||
|
|
1ed74874e5 | ||
|
|
620aeaf941 | ||
|
|
d298748b10 | ||
|
|
c46e836c28 | ||
|
|
2bcf0c9914 | ||
|
|
0e275a3e6f | ||
|
|
0d6878e5c7 | ||
|
|
24d9999fde | ||
|
|
5594c1ad2f | ||
|
|
20c44ec737 | ||
|
|
b46637f8a0 | ||
|
|
a22dbc59ac | ||
|
|
06d6693752 | ||
|
|
c28f6f05b2 | ||
|
|
10f9d5e2b1 | ||
|
|
f30789f906 | ||
|
|
b66c0580cf | ||
|
|
5db8c99b74 | ||
|
|
92c042450f | ||
|
|
4c60a3efa1 | ||
|
|
51af2838d9 | ||
|
|
70cad70766 | ||
|
|
d78214393a | ||
|
|
e62dcd5327 | ||
|
|
87ec52223a | ||
|
|
562abb6641 | ||
|
|
f894476e0e | ||
|
|
826037d499 | ||
|
|
b1ef958976 | ||
|
|
5d03c617c0 | ||
|
|
0996375c5e | ||
|
|
d927640136 | ||
|
|
1d59548df0 | ||
|
|
b8a5233a06 | ||
|
|
908efadcec | ||
|
|
3f5f752a2f | ||
|
|
7fdbe812d3 | ||
|
|
df71d7e3f0 | ||
|
|
c97b049ed0 | ||
|
|
267cd6e9f6 | ||
|
|
ef41018ac1 | ||
|
|
54f891548b | ||
|
|
b92f5a5971 | ||
|
|
8415331eee | ||
|
|
afd686f81b | ||
|
|
413c300904 | ||
|
|
bc4fb0ad21 | ||
|
|
2193c4d6e3 | ||
|
|
33fe0b74ae | ||
|
|
738f93b882 | ||
|
|
b875fcad4e | ||
|
|
c56dbba687 | ||
|
|
44783bbeb0 | ||
|
|
3428291c54 | ||
|
|
fa221e3ae5 | ||
|
|
cc23f50edf | ||
|
|
f811a028cd | ||
|
|
ff0d2cf390 | ||
|
|
c47aa4e182 | ||
|
|
1d119aad62 | ||
|
|
254b9c0a49 | ||
|
|
e760c236bc | ||
|
|
01f32af6a1 | ||
|
|
66b59ce94b | ||
|
|
69c9a4bdd0 | ||
|
|
e5ead966e9 | ||
|
|
40b7ecd2fe | ||
|
|
f6c66a9964 | ||
|
|
698d96780a | ||
|
|
b250bc0795 | ||
|
|
b229740315 | ||
|
|
a1ecc49065 | ||
|
|
bc96d30bf4 | ||
|
|
48b6acb174 | ||
|
|
d6651001fc | ||
|
|
4c9376612e | ||
|
|
c89f0e6fae | ||
|
|
d3caa2d0a9 | ||
|
|
c05a47587b | ||
|
|
10651d1d0f | ||
|
|
4e7aee0634 | ||
|
|
1065c687bc | ||
|
|
0a39857d12 | ||
|
|
72a3975a58 | ||
|
|
06d35aac0f | ||
|
|
ebc671f32f | ||
|
|
ea7cb5e323 | ||
|
|
b1ab983333 | ||
|
|
57cbedf701 | ||
|
|
6298cff1a3 | ||
|
|
a975c4d2c5 | ||
|
|
7c3f360a34 | ||
|
|
2a76fbc5a3 | ||
|
|
0f58424c73 | ||
|
|
aa1df77400 | ||
|
|
acd26ee67b | ||
|
|
11cacf9c0b | ||
|
|
351548df7c | ||
|
|
322b5da793 | ||
|
|
d6c1f38ce4 | ||
|
|
f8194708a0 | ||
|
|
c1ec6cb95d | ||
|
|
59627ebe32 | ||
|
|
e5641108ea | ||
|
|
636e996a17 | ||
|
|
bfbde5cdf4 | ||
|
|
c9aa79abaf | ||
|
|
cd8ad64a6d | ||
|
|
00e37c2b25 | ||
|
|
3263a77f97 | ||
|
|
67bb96e245 | ||
|
|
eeff14597e | ||
|
|
86a65d7344 | ||
|
|
4f48005a49 | ||
|
|
f6d7ce4356 | ||
|
|
4c5517ae94 | ||
|
|
51fb01aaf9 | ||
|
|
7ea60a1fa6 | ||
|
|
31409d6e5b | ||
|
|
483792ebb0 | ||
|
|
699b09c6c0 | ||
|
|
6bd2ec4a44 | ||
|
|
6c0a0b463f | ||
|
|
4e869bf2b0 | ||
|
|
3abc245751 | ||
|
|
b156f72783 | ||
|
|
6e8ff0104f | ||
|
|
f2f8fbbfb6 | ||
|
|
33d0b24260 | ||
|
|
cb66bcd665 | ||
|
|
5a1db38eed | ||
|
|
0a565c67dd | ||
|
|
b047ce3019 | ||
|
|
acfed81e10 | ||
|
|
581d1dac5a | ||
|
|
50b3872ae0 | ||
|
|
2ea2526858 | ||
|
|
2d9b6f38b0 | ||
|
|
a941ffa837 | ||
|
|
e2da05ac2c | ||
|
|
dd8108c974 | ||
|
|
206f8fc2b1 | ||
|
|
5a432e4ab5 | ||
|
|
83ba9222bd | ||
|
|
7e7a8b04ef | ||
|
|
a28b5012d6 | ||
|
|
85218a8fd1 | ||
|
|
590454b69e | ||
|
|
d81d48ee16 | ||
|
|
b72217eb04 | ||
|
|
8942795e76 | ||
|
|
08290e1fa5 | ||
|
|
7b45b44735 | ||
|
|
ae6913a8e0 | ||
|
|
7470ac9e16 | ||
|
|
521d10da19 | ||
|
|
98aee7bb35 | ||
|
|
d62f2c4450 | ||
|
|
95edcc3042 | ||
|
|
1bce686121 | ||
|
|
742417d405 | ||
|
|
2cfc8d528d | ||
|
|
7a4e1721c8 | ||
|
|
11d79c4874 | ||
|
|
7cd35b0a92 | ||
|
|
d0f62a26c0 | ||
|
|
01198502a3 | ||
|
|
229ad109a7 | ||
|
|
837b16d971 | ||
|
|
4010d1b93f | ||
|
|
f7ce60ae68 | ||
|
|
5e61bd5db2 | ||
|
|
a2e8a438de | ||
|
|
92904dcf55 | ||
|
|
e4f2ca630b | ||
|
|
ed80ad24c1 | ||
|
|
0c368ab84b | ||
|
|
dee2044ed6 | ||
|
|
f6f6072b3f | ||
|
|
4bfe72d750 | ||
|
|
330f59dc10 | ||
|
|
a20d981427 | ||
|
|
bd2274db75 | ||
|
|
6cfa6f4ef5 | ||
|
|
8a40d2b1b9 | ||
|
|
237958ba0f | ||
|
|
79db3a54c7 | ||
|
|
2029745f8b | ||
|
|
ea4d498502 | ||
|
|
05838f5dca | ||
|
|
79872163e2 | ||
|
|
35d0f77dd6 | ||
|
|
6660cd20bd | ||
|
|
e236ba454f | ||
|
|
6ec66e4d64 | ||
|
|
14898c0c83 | ||
|
|
d08bec9df7 | ||
|
|
9107dfa789 | ||
|
|
351f6f4d16 |
106
.env.example
@@ -1,10 +1,106 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
# AliasVault configuration file.
|
||||
#
|
||||
# Note: we recommend using the provided install.sh script to install and
|
||||
# configure AliasVault, as this will automatically set all of the following
|
||||
# variables for you and allow you to easily change them later via the CLI.
|
||||
# It also allows for easily updating AliasVault to a newer version in the
|
||||
# future.
|
||||
#
|
||||
# However if you still wish to manually install or configure AliasVault,
|
||||
# you can do so below.
|
||||
#
|
||||
# After changing settings here, make sure to restart all AliasVault
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Set the ports that your AliasVault will be accessible at.
|
||||
# These are the default ports that will be used by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these to any other ports that are available on your system.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
|
||||
# Set the hostname that your AliasVault will be accessible at.
|
||||
# E.g. `aliasvault.mydomain.com` or if you're running it on your local machine, choose `localhost`.
|
||||
HOSTNAME=
|
||||
|
||||
# Set a random 32 character string for the JWT key.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
JWT_KEY=
|
||||
|
||||
# Set the password for the data protection certificate.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
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
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Database configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# These are the credentials that are used by the PostgreSQL container
|
||||
# on startup to create the database and user, and for the application to
|
||||
# connect to the database.
|
||||
POSTGRES_DB=aliasvault
|
||||
POSTGRES_USER=aliasvault
|
||||
|
||||
# Set the password for the database user.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Note: in order to change the password for an existing installation
|
||||
# refer to https://docs.aliasvault.net/misc/dev/database-operations.html
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin user configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set the password for the admin user. This is an encrypted hash that needs
|
||||
# to be generated using the `aliasvault-cli` tool. This allows you to login
|
||||
# to the admin panel at https://your-hostname/admin.
|
||||
#
|
||||
# For example:
|
||||
# docker run --rm ghcr.io/lanedirt/aliasvault-installcli:latest hash-password "my-password"
|
||||
#
|
||||
# Then copy the output and paste it into the ADMIN_PASSWORD_HASH variable below.
|
||||
# When changing the hash, update the ADMIN_PASSWORD_GENERATED variable to the current date and time
|
||||
# and then restart the AliasVault docker containers to apply the changes.
|
||||
ADMIN_PASSWORD_HASH=
|
||||
|
||||
# Set the date and time the admin password was last generated. When changing the
|
||||
# admin password hash manually, make sure to increase this value so the system
|
||||
# knows that the password has been changed and should be overwritten with the new hash.
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Email server configuration for email aliases
|
||||
# ----------------------------------------------------------------------------
|
||||
# In order to use AliasVault's private email domains feature, you need to configure
|
||||
# your DNS. Please refer to the full documentation for more instructions on DNS:
|
||||
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
|
||||
#
|
||||
# Set the private email domains below that are allowed to be used (comma separated values).
|
||||
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
|
||||
# To disable the private email domains feature, set this to "DISABLED.TLD"
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
|
||||
# Set whether TLS is enabled for SMTP.
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Let's Encrypt configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set whether Let's Encrypt is enabled. This is only supported through
|
||||
# the install.sh script.
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Optional configuration settings
|
||||
# ----------------------------------------------------------------------------
|
||||
PUBLIC_REGISTRATION_ENABLED=true
|
||||
IP_LOGGING_ENABLED=true
|
||||
|
||||
# Set the support email address which is shown to users in the main web app.
|
||||
# Keep this blank if you don't want to show a support email.
|
||||
SUPPORT_EMAIL=
|
||||
|
||||
31
.gitattributes
vendored
@@ -1,2 +1,31 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# Set default behavior to automatically normalize line endings
|
||||
* text=auto
|
||||
|
||||
# Common files should always use LF (Unix-style) line endings
|
||||
*.sh text eol=lf
|
||||
*.cs text eol=lf
|
||||
*.razor text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Docker files should use LF
|
||||
Dockerfile text eol=lf
|
||||
docker-compose*.yml text eol=lf
|
||||
|
||||
# Config files should use LF
|
||||
*.conf text eol=lf
|
||||
*.config text eol=lf
|
||||
.env* text eol=lf
|
||||
|
||||
# Batch scripts should always use CRLF (Windows-style) line endings
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Documentation should be normalized
|
||||
*.md text
|
||||
*.txt text
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug or unexpected behavior.
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for AliasVault
|
||||
title: '[Feature Request] '
|
||||
labels: '⚡️ enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
## Description
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature enhancement
|
||||
- [ ] Documentation update
|
||||
- [ ] Other (please describe):
|
||||
|
||||
## Related Issues
|
||||
Fixes #[issue-number]
|
||||
|
||||
## Checklist
|
||||
- [ ] Code adheres to project standards and guidelines.
|
||||
- [ ] Documentation has been updated where applicable.
|
||||
|
||||
## Additional Information
|
||||
Add any additional context, screenshots, or explanations here.
|
||||
10
.github/hooks/commit-msg
vendored
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Commit-msg hook to check commit messages for issue number in format "(#123)"
|
||||
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if ! grep -q "(\#[0-9]\+)" <<< "$commit_message"; then
|
||||
echo "Error: Commit message must contain an issue number in the format \"(#123)\""
|
||||
exit 1
|
||||
fi
|
||||
168
.github/workflows/browser-extension-build.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
name: Browser Extension Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-chrome-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:chrome
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Chrome Extension
|
||||
run: npm run zip:chrome
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/chrome-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-chrome.zip -d dist/chrome-unpacked
|
||||
|
||||
- name: Upload dist artifact Chrome
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-chrome
|
||||
path: browser-extension/dist/chrome-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
build-firefox-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:firefox
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Firefox Extension
|
||||
run: npm run zip:firefox
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/firefox-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-firefox.zip -d dist/firefox-unpacked
|
||||
mkdir -p dist/sources-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-sources.zip -d dist/sources-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-firefox
|
||||
path: browser-extension/dist/firefox-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox sources
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-sources
|
||||
path: browser-extension/dist/sources-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
build-edge-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:edge
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Edge Extension
|
||||
run: npm run zip:edge
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/edge-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-edge.zip -d dist/edge-unpacked
|
||||
|
||||
- name: Upload dist artifact Edge
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-edge
|
||||
path: browser-extension/dist/edge-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
8
.github/workflows/docker-compose-build.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -92,9 +96,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect"
|
||||
echo "Expected: 'New admin password: <at least 8 base64 chars>'"
|
||||
|
||||
74
.github/workflows/docker-compose-pull.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-docker:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -20,8 +24,16 @@ jobs:
|
||||
- name: Get repository and branch information
|
||||
id: repo-info
|
||||
run: |
|
||||
echo "REPO_FULL_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
# Check if this is a PR from a fork
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
# If PR is from a fork, use main branch from lanedirt/AliasVault
|
||||
echo "REPO_FULL_NAME=lanedirt/AliasVault" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=main" >> $GITHUB_ENV
|
||||
else
|
||||
# Otherwise use the current repository and branch
|
||||
echo "REPO_FULL_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Download install script from current branch
|
||||
run: |
|
||||
@@ -34,32 +46,55 @@ jobs:
|
||||
echo "SMTP_PORT=2525" > .env
|
||||
|
||||
- name: Set permissions and run install.sh
|
||||
id: install_script
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh install --verbose
|
||||
{
|
||||
./install.sh install --verbose
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Force success exit code
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
false # Propagate failure
|
||||
fi
|
||||
} || {
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Version mismatch is okay
|
||||
else
|
||||
exit $exit_code # Propagate other failures
|
||||
fi
|
||||
}
|
||||
|
||||
- name: Set up Docker Compose
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
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 (WASM app) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
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
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -74,6 +109,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/admin (Admin) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -88,6 +124,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:2525 (SmtpService) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -100,9 +137,10 @@ jobs:
|
||||
echo "SmtpService responded on port 2525"
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-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"
|
||||
|
||||
6
.github/workflows/dotnet-e2e-admin-tests.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
admin-tests:
|
||||
timeout-minutes: 60
|
||||
@@ -40,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: admin-test-results
|
||||
path: TestResults-Admin.xml
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
client-tests:
|
||||
timeout-minutes: 60
|
||||
|
||||
46
.github/workflows/dotnet-e2e-misc-tests.yml
vendored
@@ -1,46 +0,0 @@
|
||||
# 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: Start dev database
|
||||
run: ./install.sh configure-dev-db start
|
||||
|
||||
- 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
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
|
||||
4
.github/workflows/dotnet-unit-tests.yml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# This workflow will publish new Docker images to the GitHub Container Registry when a new release is published.
|
||||
name: Publish Docker Images
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -11,7 +10,56 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
upload-install-script:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload install.sh to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: install.sh
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
package-browser-extensions:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
npm run zip:chrome
|
||||
npm run zip:firefox
|
||||
npm run zip:edge
|
||||
|
||||
- name: Upload extensions to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-firefox.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-sources.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-and-push-docker:
|
||||
needs: [upload-install-script, package-browser-extensions]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -21,6 +69,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
@@ -43,6 +97,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Databases/AliasServerDb/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:${{ github.ref_name }}
|
||||
|
||||
@@ -51,6 +106,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:${{ github.ref_name }}
|
||||
|
||||
@@ -59,6 +115,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:${{ github.ref_name }}
|
||||
|
||||
@@ -67,6 +124,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Admin/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
|
||||
|
||||
@@ -75,6 +133,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
|
||||
|
||||
@@ -83,6 +142,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
|
||||
|
||||
@@ -91,6 +151,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.TaskRunner/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
|
||||
|
||||
@@ -99,5 +160,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
22
.github/workflows/sonarcloud-code-analysis.yml
vendored
@@ -1,11 +1,15 @@
|
||||
# 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.
|
||||
# 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. The "pull_request_target" event is
|
||||
# used to ensure that the analysis is done on the source branch of the pull request which has
|
||||
# access to the SonarCloud token secret.
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
@@ -23,11 +27,13 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
distribution: 'zulu'
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code of PR branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
@@ -57,7 +63,11 @@ jobs:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html"
|
||||
} else {
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
5
.gitignore
vendored
@@ -9,6 +9,7 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.code-workspace
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
@@ -376,6 +377,10 @@ FodyWeavers.xsd
|
||||
.idea
|
||||
*.licenseheader
|
||||
|
||||
# Junie JetBrains plugin
|
||||
.junie
|
||||
.output.txt
|
||||
|
||||
# Codebuddy Rider plugin
|
||||
.codebuddy
|
||||
|
||||
|
||||
4
.vscode/launch.json
vendored
@@ -2,10 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "C#: AliasVault.WebApp [http]",
|
||||
"name": "C#: AliasVault.Client [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.WebApp/AliasVault.WebApp.csproj",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.Client/AliasVault.Client.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
},
|
||||
{
|
||||
|
||||
116
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build and watch API",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Api"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Admin",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "build:client-css"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Client"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Admin CSS",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "build:admin-css"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Admin"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client (API + Client + CSS)",
|
||||
"dependsOn": [
|
||||
"Build and watch API",
|
||||
"Build and watch Client",
|
||||
"Build and watch Client CSS"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Unit Tests",
|
||||
"type": "shell",
|
||||
"command": "dotnet",
|
||||
"args": ["test"],
|
||||
"problemMatcher": "$msCompile",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/Tests/AliasVault.UnitTests"
|
||||
},
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Browser Extension (Chrome Dev)",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "dev:chrome"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/browser-extension"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
contact@support.aliasvault.net.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing to the source code
|
||||
We welcome contributions to AliasVault. Please read the guidelines on the official AliasVault docs website on how to get your local development environment setup and the general contribution guidelines:
|
||||
|
||||
https://docs.aliasvault.net/misc/dev/contributing.html
|
||||
https://docs.aliasvault.net/misc/dev/
|
||||
|
||||
> Tip: if the URL above is not available, the raw doc pages can also be found in the `docs` folder in this repository.
|
||||
|
||||
|
||||
136
README.md
@@ -1,84 +1,92 @@
|
||||
<div align="center">
|
||||
|
||||
🌟 **If you find this project useful, please consider giving it a star!** 🌟
|
||||
|
||||
<h1><img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="40" /> AliasVault</h1>
|
||||
|
||||
<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="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> • <a href="#installation">Installation ⚙️</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Open-source password and alias manager</strong>
|
||||
</p>
|
||||
# AliasVault: password & (email) alias manager [<img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="100" align="right" alt="AliasVault">](https://github.com/lanedirt/AliasVault)
|
||||
|
||||
[<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-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>
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
<div align="center">
|
||||
> AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. Use the official supported cloud version or self-host AliasVault on your own server with Docker.
|
||||
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=join%20discord%20chat&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
</div>
|
||||
|
||||
AliasVault is an end-to-end encrypted password and alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. The core of AliasVault is built with C# ASP.NET Blazor WASM technology. AliasVault can be self-hosted on your own server with Docker.
|
||||
- <a href="https://app.aliasvault.net">Try the cloud version 🔥</a> - <a href="https://aliasvault.net?utm_source=gh-readme">Website 🌐</a> - <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> - <a href="#self-hosting">Self-host instructions ⚙️</a> - <a href="https://aliasvault.net/plugins?utm_source=gh-readme">Browser Extensions 🔌</a>
|
||||
|
||||
### 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.
|
||||
- **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 real working email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app and browser extension.
|
||||
- **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 & Self-hostable**:
|
||||
- The source code is available on GitHub and AliasVault can be self-hosted on your own server via an easy install script.
|
||||
|
||||
> 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.
|
||||
## Screenshots
|
||||
|
||||
## 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.
|
||||
<table>
|
||||
<tr>
|
||||
<th align="center">Browser Extension</th>
|
||||
<th align="center">Generate email and aliases</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/d9ffd3dc-08a0-462d-8148-e8da5ec5a520" alt="Browser Autofill" />
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/86752994-d469-4b0e-b633-c089e0aed12b" alt="Generate Aliases" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="center">Strong security</th>
|
||||
<th align="center">Easy self-host</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/26b66379-10a5-4b8b-9c69-e64b553a10be" alt="Strong security" />
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://github.com/user-attachments/assets/47c7002a-e326-4507-8801-194e134e00dd" alt="Easy self-host installation" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">
|
||||
## Official Cloud Version
|
||||
The official cloud version of AliasVault is freely available at [app.aliasvault.net](https://app.aliasvault.net). This fully supported platform is always up to date with our latest release. Create an account to protect your privacy today.
|
||||
|
||||
## Installation
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
To install AliasVault, the easiest method is to use the provided install script. This will download the pre-built Docker images and start the containers.
|
||||
## Self-hosting
|
||||
For full control over your own data you can self-host and install AliasVault on your own servers. The easiest method is to use the provided install script. This will download the pre-built Docker images and start the containers.
|
||||
|
||||
### 1. Install using install script
|
||||
### Install using install script
|
||||
|
||||
This method uses pre-built Docker images and works on minimal hardware specifications:
|
||||
|
||||
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
|
||||
- Linux VM with root access (Ubuntu/AlmaLinux recommended) or Raspberry Pi
|
||||
- 1 vCPU
|
||||
- 1GB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
```bash
|
||||
# Download install script
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
# Download install script from latest stable release
|
||||
curl -L -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/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. Post-Installation
|
||||
|
||||
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 `.env` file.
|
||||
|
||||
## Detailed documentation
|
||||
For more detailed information about the installation process and other topics, please see the official documentation website:
|
||||
## Documentation
|
||||
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
|
||||
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
|
||||
|
||||
Here you can also find step-by-step instructions on how to install AliasVault to e.g. Azure, AWS and other popular cloud providers.
|
||||
|
||||
## Security Architecture
|
||||
<a href="https://docs.aliasvault.net/architecture"><img alt="AliasVault Security Architecture Diagram" src="docs/assets/diagrams/security-architecture/aliasvault-security-architecture-thumb.jpg" width="343"></a>
|
||||
|
||||
@@ -92,21 +100,35 @@ For detailed information about our encryption implementation and security archit
|
||||
- [SECURITY.md](SECURITY.md)
|
||||
- [Security Architecture Diagram](https://docs.aliasvault.net/architecture)
|
||||
|
||||
## Roadmap
|
||||
|
||||
AliasVault is under active development, with a strong focus on usability, security, and cross-platform support.
|
||||
The main focus is on ensuring robust usability for everyday tasks, including comprehensive autofill capabilities across all platforms.
|
||||
|
||||
## Tech stack / credits
|
||||
The following technologies, frameworks and libraries are used in this project:
|
||||
🛠️ Incremental releases are published every 2–3 weeks, with a strong emphasis on real-world testing and user feedback.
|
||||
During this phase, AliasVault can safely be used in production as it maintains strict data integrity and automatic migration guarantees.
|
||||
|
||||
Core features that are being worked on:
|
||||
|
||||
- [x] Core password & alias management
|
||||
- [x] Full end-to-end encryption
|
||||
- [x] Built-in email server for aliases
|
||||
- [x] Easy self-hosted installer
|
||||
- [x] Browser extensions with autofill feature (Chrome, Firefox, Edge, Safari, Brave)
|
||||
- [x] Built-in TOTP authenticator
|
||||
- [x] Import passwords from traditional password managers
|
||||
- [ ] iOS and Android native apps
|
||||
- [ ] Data model improvements to support reusable identities in combination with aliases
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/lanedirt/AliasVault/issues/731)
|
||||
|
||||
### Got feedback or ideas?
|
||||
Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)! Contributions are warmly welcomed—whether in feature development, testing, or spreading the word. Get in touch on Discord if you're interested in contributing.
|
||||
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
- [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) - A simple, modern, object-oriented, and type-safe programming language.
|
||||
- [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.
|
||||
|
||||
@@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests.Client.
|
||||
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}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.ImportExport", "src\Utilities\AliasVault.ImportExport\AliasVault.ImportExport.csproj", "{A9C9A606-C87E-4298-AB32-09B1884D7487}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{8A477241-B96C-4174-968D-D40CB77F1ECD}"
|
||||
EndProject
|
||||
@@ -59,8 +59,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Cryptography.Cli
|
||||
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}"
|
||||
@@ -161,10 +159,6 @@ Global
|
||||
{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
|
||||
@@ -188,6 +182,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{ED328644-A152-403D-86EB-81201AA07744} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{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}
|
||||
@@ -198,16 +193,14 @@ Global
|
||||
{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}
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{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}
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
|
||||
28
browser-extension/.editorconfig
Normal file
@@ -0,0 +1,28 @@
|
||||
# Child EditorConfig file that enforces 2 space indent for Typescript projects
|
||||
root = false
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# TypeScript and JavaScript files
|
||||
[*.{ts,tsx,js,jsx}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# YAML files
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
33
browser-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
dist
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Dictionaries
|
||||
# During build these are copied from the ../dictionaries folder because firefox zip requires all files to be in the root of the zip.
|
||||
# Therefore this copied folder is not committed to the repo the original folder is already available outside this directory.
|
||||
# See vite-plugins/identity-gen-dict-loader.ts for more details.
|
||||
dictionaries
|
||||
25
browser-extension/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
This folder contains the source code for the browser extensions for AliasVault.
|
||||
|
||||
The browser extension is built using WXT and React:
|
||||
- [WXT](https://wxt.dev/) is a build tool for browser extensions.
|
||||
- [React](https://reactjs.org/) is a JavaScript library for building user interfaces.
|
||||
|
||||
To build the browser extension, run the following command in this directory:
|
||||
|
||||
### Build the browser extension
|
||||
```bash
|
||||
npm install
|
||||
|
||||
# Build the Chrome extension (saves in dist/chrome-mv3)
|
||||
npm run zip:chrome
|
||||
|
||||
# Build the Firefox extension (creates two zip files in dist)
|
||||
npm run zip:firefox
|
||||
|
||||
# Build the Edge extension (saves in dist/edge-mv3)
|
||||
npm run zip:edge
|
||||
|
||||
# Build the Safari extension (saves in dist/safari-mv2 which is referenced by the dist/safari-xcode/AliasVault.xcodeproj project)
|
||||
npm run build:safari
|
||||
# Open the dist/safari-xcode/AliasVault.xcodeproj project in MacOS Xcode and run the project. This will install the extension to your Safari browser locally.
|
||||
```
|
||||
129
browser-extension/eslint.config.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import js from "@eslint/js";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import jsdocPlugin from "eslint-plugin-jsdoc";
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: ".",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin,
|
||||
"react": reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
"import": importPlugin,
|
||||
"jsdoc": jsdocPlugin,
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...reactPlugin.configs.recommended.rules,
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
"curly": ["error", "all"],
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": ["error", {
|
||||
"ignoreTernaryTests": false,
|
||||
"ignoreConditionalTests": false,
|
||||
"ignoreMixedLogicalExpressions": false
|
||||
}],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/no-unused-prop-types": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {
|
||||
"vars": "all",
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": true,
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1,
|
||||
"VariableDeclarator": 1,
|
||||
"outerIIFEBody": 1,
|
||||
"MemberExpression": 1,
|
||||
"FunctionDeclaration": { "parameters": 1, "body": 1 },
|
||||
"FunctionExpression": { "parameters": 1, "body": 1 },
|
||||
"CallExpression": { "arguments": 1 },
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1,
|
||||
"ImportDeclaration": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoreComments": false
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1, "maxBOF": 0 }],
|
||||
"no-console": ["error", { allow: ["warn", "error", "info", "debug"] }],
|
||||
"jsdoc/require-jsdoc": ["error", {
|
||||
"require": {
|
||||
"FunctionDeclaration": true,
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true,
|
||||
"ArrowFunctionExpression": true,
|
||||
"FunctionExpression": true
|
||||
}
|
||||
}],
|
||||
"jsdoc/require-description": ["error", {
|
||||
"contexts": [
|
||||
"FunctionDeclaration",
|
||||
"MethodDefinition",
|
||||
"ClassDeclaration",
|
||||
"ArrowFunctionExpression",
|
||||
"FunctionExpression"
|
||||
]
|
||||
}],
|
||||
"spaced-comment": ["error", "always"],
|
||||
"multiline-comment-style": ["error", "starred-block"],
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": ["error"],
|
||||
"@typescript-eslint/typedef": ["error"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"prefix": ["I"]
|
||||
},
|
||||
{
|
||||
"selector": "class",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/jsx-no-constructed-context-values": "error",
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
NodeJS: true,
|
||||
chrome: 'readonly',
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
13494
browser-extension/package-lock.json
generated
Normal file
64
browser-extension/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"dev:edge": "wxt -b edge",
|
||||
"build:chrome": "wxt build -b chrome",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"build:edge": "wxt build -b edge",
|
||||
"build:safari": "wxt build -b safari",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint src",
|
||||
"lint:custom": "eslint",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"zip": "wxt zip",
|
||||
"zip:chrome": "wxt zip -b chrome",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"zip:edge": "wxt zip -b edge",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.8",
|
||||
"webext-bridge": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"wxt": "^0.19.13"
|
||||
}
|
||||
}
|
||||
6
browser-extension/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
62
browser-extension/safari-xcode/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.Safari.web-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SafariWebExtensionHandler.swift
|
||||
// AliasVault Extension
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import os.log
|
||||
|
||||
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
func beginRequest(with context: NSExtensionContext) {
|
||||
let request = context.inputItems.first as? NSExtensionItem
|
||||
|
||||
let profile: UUID?
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
|
||||
} else {
|
||||
profile = request?.userInfo?["profile"] as? UUID
|
||||
}
|
||||
|
||||
let message: Any?
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
message = request?.userInfo?[SFExtensionMessageKey]
|
||||
} else {
|
||||
message = request?.userInfo?["message"]
|
||||
}
|
||||
|
||||
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
|
||||
|
||||
let response = NSExtensionItem()
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
|
||||
} else {
|
||||
response.userInfo = [ "message": [ "echo": message ] ]
|
||||
}
|
||||
|
||||
context.completeRequest(returningItems: [ response ], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */; };
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAA2D81A9F7006174AB /* Base */; };
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAC2D81A9F7006174AB /* Icon.png */; };
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAE2D81A9F7006174AB /* Style.css */; };
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB02D81A9F7006174AB /* Script.js */; };
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFB22D81A9F7006174AB /* ViewController.swift */; };
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB52D81A9F7006174AB /* Base */; };
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB72D81A9F8006174AB /* Assets.xcassets */; };
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */; };
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD32D81A9F8006174AB /* background.js */; };
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD42D81A9F8006174AB /* popup.html */; };
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD52D81A9F8006174AB /* chunks */; };
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD62D81A9F8006174AB /* content-scripts */; };
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD72D81A9F8006174AB /* manifest.json */; };
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD82D81A9F8006174AB /* icon */; };
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD92D81A9F8006174AB /* assets */; };
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFDA2D81A9F8006174AB /* src */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = CE0CAFBF2D81A9F8006174AB;
|
||||
remoteInfo = "AliasVault Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AliasVault.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = "<group>"; };
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = "<group>"; };
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = "<group>"; };
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
CE0CAFB52D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "AliasVault Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault_Extension.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = background.js; path = "../../../dist/safari-mv2/background.js"; sourceTree = "<group>"; };
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = popup.html; path = "../../../dist/safari-mv2/popup.html"; sourceTree = "<group>"; };
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = chunks; path = "../../../dist/safari-mv2/chunks"; sourceTree = "<group>"; };
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "content-scripts"; path = "../../../dist/safari-mv2/content-scripts"; sourceTree = "<group>"; };
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = "../../../dist/safari-mv2/manifest.json"; sourceTree = "<group>"; };
|
||||
CE0CAFD82D81A9F8006174AB /* icon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icon; path = "../../../dist/safari-mv2/icon"; sourceTree = "<group>"; };
|
||||
CE0CAFD92D81A9F8006174AB /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist/safari-mv2/assets"; sourceTree = "<group>"; };
|
||||
CE0CAFDA2D81A9F8006174AB /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist/safari-mv2/src"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CE0CAF9A2D81A9F7006174AB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */,
|
||||
CE0CAFA42D81A9F7006174AB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA42D81A9F7006174AB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */,
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */,
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */,
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */,
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */,
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */,
|
||||
);
|
||||
path = AliasVault;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */,
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */,
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */,
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */,
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */,
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */,
|
||||
);
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */,
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */,
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */,
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */,
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */,
|
||||
CE0CAFD82D81A9F8006174AB /* icon */,
|
||||
CE0CAFD92D81A9F8006174AB /* assets */,
|
||||
CE0CAFDA2D81A9F8006174AB /* src */,
|
||||
);
|
||||
name = Resources;
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */;
|
||||
buildPhases = (
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */,
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */,
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */,
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */,
|
||||
);
|
||||
name = AliasVault;
|
||||
productName = AliasVault;
|
||||
productReference = CE0CAFA32D81A9F7006174AB /* AliasVault.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */;
|
||||
buildPhases = (
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */,
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */,
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "AliasVault Extension";
|
||||
productName = "AliasVault Extension";
|
||||
productReference = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CE0CAF9B2D81A9F7006174AB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
CE0CAFA22D81A9F7006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CE0CAF9A2D81A9F7006174AB;
|
||||
productRefGroup = CE0CAFA42D81A9F7006174AB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */,
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */,
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */,
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */,
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */,
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */,
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */,
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */,
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */,
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */,
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */,
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */,
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */,
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */;
|
||||
targetProxy = CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.html;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFB52D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.16.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFD12D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.16.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */,
|
||||
CE0CAFD12D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Override point for customization after application launch.
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>AliasVault</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="stylesheet" href="../Style.css">
|
||||
<script src="../Script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<img src="../Icon.png" width="128" height="128" alt="AliasVault Icon">
|
||||
<p class="state-unknown">To enable AliasVault’s browser extension, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-on">AliasVault’s browser extension is currently enabled in Safari. If you wish to turn it off, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-off">AliasVault’s browser extension is currently disabled in Safari. If you wish to turn it on, go to the Safari Extensions preferences.</p>
|
||||
<button class="open-preferences">Open Safari Extensions Preferences…</button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19085"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="19085"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AliasVault" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AliasVault" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AliasVault" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Hide AliasVault" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit AliasVault" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AliasVault Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="76" y="-134"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="AliasVault" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
</wkWebView>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SFSafariWebExtensionConverterVersion</key>
|
||||
<string>15.4</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
||||
function show(enabled, useSettingsInsteadOfPreferences) {
|
||||
if (useSettingsInsteadOfPreferences) {
|
||||
document.getElementsByClassName('state-on')[0].innerText = "AliasVault's Safari browser extension is succesfully enabled. If you wish to turn it off, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-off')[0].innerText = "AliasVault's Safari browser extension is currently disabled. If you wish to turn it on, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-unknown')[0].innerText = "To enable AliasVault's Safari browser extension, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Extensions Preferences…";
|
||||
}
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
document.body.classList.toggle(`state-on`, enabled);
|
||||
document.body.classList.toggle(`state-off`, !enabled);
|
||||
} else {
|
||||
document.body.classList.remove(`state-on`);
|
||||
document.body.classList.remove(`state-off`);
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
webkit.messageHandlers.controller.postMessage("open-preferences");
|
||||
}
|
||||
|
||||
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
|
||||
@@ -0,0 +1,44 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--spacing: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing);
|
||||
margin: 0 calc(var(--spacing) * 2);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font: -apple-system-short-body;
|
||||
font-family: -apple-system-short-body, system-ui;
|
||||
}
|
||||
|
||||
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-on :is(.state-off, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-off :is(.state-on, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
let extensionBundleIdentifier = "net.aliasvault.safari.extension"
|
||||
|
||||
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
|
||||
@IBOutlet var webView: WKWebView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
|
||||
self.webView.configuration.userContentController.add(self, name: "controller")
|
||||
|
||||
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
|
||||
guard let state = state, error == nil else {
|
||||
// Insert code to inform the user that something went wrong.
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if #available(macOS 13, *) {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
|
||||
} else {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
if (message.body as! String != "open-preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
// Show manual instructions in case opening the preferences fails due to restricted permissions.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Safari Extensions Settings"
|
||||
alert.informativeText = """
|
||||
Please follow these steps to enable the extension:
|
||||
1. Open Safari
|
||||
2. Click Safari > Settings in the menu bar
|
||||
3. Go to Extensions
|
||||
4. Find and enable "AliasVault"
|
||||
"""
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.runModal()
|
||||
}
|
||||
else {
|
||||
// Close app
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
browser-extension/safari-xcode/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
This folder contains the Xcode project used to publish the Safari version of the AliasVault browser extension to Apple.
|
||||
|
||||
This project was created using the `safari-web-extension-converter` tool. This XCode project is a simple wrapper around the
|
||||
WXT React browser extension, which is required by Apple in order to package and submit a Safari extension.
|
||||
|
||||
For more information see:
|
||||
- https://developer.apple.com/documentation/safariservices/converting-a-web-extension-for-safari
|
||||
- https://developer.apple.com/documentation/safariservices/running-your-safari-web-extension
|
||||
|
||||
To recreate this project, run the following command in the browser-extension root directory:
|
||||
|
||||
```bash
|
||||
# Build the Safari extension via the normal build process (outputs in dist/safari-mv2)
|
||||
npm run build:safari
|
||||
|
||||
# Convert the safari extension to an Xcode project (requires MacOS/XCode command line interface)
|
||||
xcrun safari-web-extension-converter --bundle-identifier net.aliasvault.safari --macos-only dist/safari-mv2 --project-location safari-xcode --force
|
||||
|
||||
# After the Xcode project is opened, you can run the extension by clicking the "Run" button in the top left corner of the Xcode window.
|
||||
# This will install the extension to your Safari browser and allow you to run it.
|
||||
```
|
||||
|
||||
> Note: This project does not need to be recreated when the extension is updated. It loads all extension files from the dist/safari-mv2 directory that is created by the `build:safari` command. To update the extension and/or publish a new version:
|
||||
> 1. Run `npm run build:safari` to rebuild the Safari extension
|
||||
> 2. Open this Xcode project and rebuild it to get the latest version
|
||||
> 3. Submit the extension to Apple for review via Xcode:
|
||||
> - Select the "Archive" option from the Product menu
|
||||
> - Select the newly created archive and click "Distribute App"
|
||||
> - Select "Distribute" and follow the instructions to submit to App Store Connect
|
||||
1
browser-extension/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
browser-extension/src/assets/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
34
browser-extension/src/entrypoints/background.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
|
||||
|
||||
export default defineBackground({
|
||||
/**
|
||||
* This is the main entry point for the background script.
|
||||
*/
|
||||
main() {
|
||||
// Set up context menus
|
||||
setupContextMenus();
|
||||
browser.contextMenus.onClicked.addListener((info: browser.contextMenus.OnClickData, tab?: browser.tabs.Tab) =>
|
||||
handleContextMenuClick(info, tab)
|
||||
);
|
||||
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_LANGUAGE', () => handleGetDefaultIdentityLanguage());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
}
|
||||
});
|
||||
114
browser-extension/src/entrypoints/background/ContextMenu.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { sendMessage } from 'webext-bridge/background';
|
||||
import { PasswordGenerator } from '../../utils/generators/Password/PasswordGenerator';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
/**
|
||||
* Setup the context menus.
|
||||
*/
|
||||
export function setupContextMenus() : void {
|
||||
// Create root menu
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-root",
|
||||
title: "AliasVault",
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
// Add fill option first (only for editable fields)
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-activate-form",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Autofill with AliasVault",
|
||||
contexts: ["editable"],
|
||||
});
|
||||
|
||||
// Add separator (only for editable fields)
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-separator",
|
||||
parentId: "aliasvault-root",
|
||||
type: "separator",
|
||||
contexts: ["editable"],
|
||||
});
|
||||
|
||||
// Add password generator option
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-generate-password",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Generate random password (copy to clipboard)",
|
||||
contexts: ["all"]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle context menu clicks.
|
||||
*/
|
||||
export function handleContextMenuClick(info: browser.contextMenus.OnClickData, tab?: browser.tabs.Tab) : void {
|
||||
if (info.menuItemId === "aliasvault-generate-password") {
|
||||
// Initialize password generator
|
||||
const passwordGenerator = new PasswordGenerator();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
// Use browser.scripting to write password to clipboard from active tab
|
||||
if (tab?.id) {
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: copyPasswordToClipboard,
|
||||
args: [password]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
// First get the active element's identifier
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: getActiveElementIdentifier,
|
||||
}, (results) => {
|
||||
const elementIdentifier = results[0]?.result;
|
||||
if (elementIdentifier) {
|
||||
// Send message to content script with proper tab targeting
|
||||
sendMessage('OPEN_AUTOFILL_POPUP', { elementIdentifier }, `content-script@${tab.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy provided password to clipboard.
|
||||
*/
|
||||
function copyPasswordToClipboard(generatedPassword: string) : void {
|
||||
navigator.clipboard.writeText(generatedPassword).then(() => {
|
||||
showToast('Password copied to clipboard');
|
||||
});
|
||||
|
||||
/**
|
||||
* Show a toast notification.
|
||||
*/
|
||||
function showToast(message: string) : void {
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 16px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
setTimeout(() => notification.remove(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate AliasVault for the active input element.
|
||||
*/
|
||||
function getActiveElementIdentifier() : string {
|
||||
const target = document.activeElement;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
return target.id || target.name || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { browser } from "wxt/browser";
|
||||
import { BoolResponse } from '../../utils/types/messaging/BoolResponse';
|
||||
/**
|
||||
* Handle opening the popup.
|
||||
*/
|
||||
export function handleOpenPopup() : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL('/popup.html?mode=inline_unlock&expanded=true'),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the popup with a credential.
|
||||
*/
|
||||
export function handlePopupWithCredential(message: any) : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL(`/popup.html?expanded=true#/credentials/${message.credentialId}`),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import EncryptionUtility from '../../utils/EncryptionUtility';
|
||||
import SqliteClient from '../../utils/SqliteClient';
|
||||
import { WebApiService } from '../../utils/WebApiService';
|
||||
import { Vault } from '../../utils/types/webapi/Vault';
|
||||
import { VaultResponse } from '../../utils/types/webapi/VaultResponse';
|
||||
import { VaultPostResponse } from '../../utils/types/webapi/VaultPostResponse';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { BoolResponse as messageBoolResponse } from '../../utils/types/messaging/BoolResponse';
|
||||
import { VaultResponse as messageVaultResponse } from '../../utils/types/messaging/VaultResponse';
|
||||
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
|
||||
import { StringResponse as stringResponse } from '../../utils/types/messaging/StringResponse';
|
||||
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '../../utils/types/messaging/PasswordSettingsResponse';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the vault in browser storage.
|
||||
*/
|
||||
export async function handleStoreVault(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
try {
|
||||
const vaultResponse = message.vaultResponse as VaultResponse;
|
||||
const encryptedVaultBlob = vaultResponse.vault.blob;
|
||||
|
||||
// Store encrypted vault and derived key in session storage.
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVaultBlob },
|
||||
{ key: 'session:derivedKey', value: message.derivedKey },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store vault:', error);
|
||||
return { success: false, error: 'Failed to store vault' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the vault with the server to check if a newer vault is available. If so, the vault will be updated.
|
||||
*/
|
||||
export async function handleSyncVault(
|
||||
) : Promise<messageBoolResponse> {
|
||||
const webApi = new WebApiService(() => {});
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
return { success: false, error: statusError };
|
||||
}
|
||||
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
// Retrieve the latest vault from the server.
|
||||
const vaultResponse = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: vaultResponse.vault.blob },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the vault from browser storage.
|
||||
*/
|
||||
export async function handleGetVault(
|
||||
) : Promise<messageVaultResponse> {
|
||||
try {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
if (!encryptedVault) {
|
||||
console.error('Vault not available');
|
||||
return { success: false, error: 'Vault not available' };
|
||||
}
|
||||
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
vault: decryptedVault,
|
||||
publicEmailDomains: publicEmailDomains ?? [],
|
||||
privateEmailDomains: privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: vaultRevisionNumber ?? 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get vault:', error);
|
||||
return { success: false, error: 'Failed to get vault' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the vault from browser storage.
|
||||
*/
|
||||
export function handleClearVault(
|
||||
) : messageBoolResponse {
|
||||
storage.removeItems([
|
||||
'session:encryptedVault',
|
||||
'session:derivedKey',
|
||||
'session:publicEmailDomains',
|
||||
'session:privateEmailDomains',
|
||||
'session:vaultRevisionNumber'
|
||||
]);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all credentials.
|
||||
*/
|
||||
export async function handleGetCredentials(
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const credentials = sqliteClient.getAllCredentials();
|
||||
return { success: true, credentials: credentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting credentials:', error);
|
||||
return { success: false, error: 'Failed to get credentials' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identity.
|
||||
*/
|
||||
export async function handleCreateIdentity(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Add the new credential to the vault/database.
|
||||
sqliteClient.createCredential(message.credential);
|
||||
|
||||
// Upload the new vault to the server.
|
||||
await uploadNewVaultToServer(sqliteClient);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to create identity:', error);
|
||||
return { success: false, error: 'Failed to create identity' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email addresses for a vault.
|
||||
*/
|
||||
export async function getEmailAddressesForVault(
|
||||
sqliteClient: SqliteClient
|
||||
): Promise<string[]> {
|
||||
// TODO: create separate query to only get email addresses to avoid loading all credentials.
|
||||
const credentials = sqliteClient.getAllCredentials();
|
||||
|
||||
// Get metadata from storage
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
|
||||
const emailAddresses = credentials
|
||||
.filter(cred => cred.Alias?.Email != null)
|
||||
.map(cred => cred.Alias.Email ?? '')
|
||||
.filter((email, index, self) => self.indexOf(email) === index);
|
||||
|
||||
return emailAddresses.filter(email => {
|
||||
const domain = email?.split('@')[1];
|
||||
return domain && privateEmailDomains.includes(domain);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default email domain for a vault.
|
||||
*/
|
||||
export function handleGetDefaultEmailDomain(
|
||||
) : Promise<stringResponse> {
|
||||
return (async () : Promise<stringResponse> => {
|
||||
try {
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain();
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string) : boolean => {
|
||||
const isValid = (domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain))) as boolean;
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return { success: true, value: defaultEmailDomain };
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPrivate) {
|
||||
return { success: true, value: firstPrivate };
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPublic) {
|
||||
return { success: true, value: firstPublic };
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: 'Failed to get default email domain' };
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity language.
|
||||
*/
|
||||
export async function handleGetDefaultIdentityLanguage(
|
||||
) : Promise<stringResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const settingValue = sqliteClient.getDefaultIdentityLanguage();
|
||||
|
||||
return { success: true, value: settingValue };
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity language:', error);
|
||||
return { success: false, error: 'Failed to get default identity language' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings.
|
||||
*/
|
||||
export async function handleGetPasswordSettings(
|
||||
) : Promise<messagePasswordSettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const passwordSettings = sqliteClient.getPasswordSettings();
|
||||
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: 'Failed to get password settings' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the derived key for the encrypted vault.
|
||||
*/
|
||||
export async function handleGetDerivedKey(
|
||||
) : Promise<string> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new version of the vault to the server using the provided sqlite client.
|
||||
*/
|
||||
async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void> {
|
||||
const updatedVaultData = sqliteClient.exportToBase64();
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
const encryptedVault = await EncryptionUtility.symmetricEncrypt(
|
||||
updatedVaultData,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVault }
|
||||
]);
|
||||
|
||||
// Get metadata from storage
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
// Upload new encrypted vault to server.
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
const emailAddresses = await getEmailAddressesForVault(sqliteClient);
|
||||
|
||||
const newVault: Vault = {
|
||||
blob: encryptedVault,
|
||||
createdAt: new Date().toISOString(),
|
||||
credentialsCount: sqliteClient.getAllCredentials().length,
|
||||
currentRevisionNumber: vaultRevisionNumber,
|
||||
emailAddressList: emailAddresses,
|
||||
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
encryptionPublicKey: '', // Empty on purpose, only required if new public/private key pair is generated.
|
||||
client: '', // Empty on purpose, API will not use this for vault updates.
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: sqliteClient.getDatabaseVersion() ?? '0.0.0'
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
const response = await webApi.post<Vault, VaultPostResponse>('Vault', newVault);
|
||||
|
||||
// Check if response is successful (.status === 0)
|
||||
if (response.status === 0) {
|
||||
await storage.setItem('session:vaultRevisionNumber', response.newRevisionNumber);
|
||||
} else {
|
||||
throw new Error('Failed to upload new vault to server');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new sqlite client for the stored vault.
|
||||
*/
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!encryptedVault || !derivedKey) {
|
||||
throw new Error('No vault or derived key found');
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client with the decrypted vault.
|
||||
const sqliteClient = new SqliteClient();
|
||||
await sqliteClient.initializeFromBase64(decryptedVault);
|
||||
|
||||
return sqliteClient;
|
||||
}
|
||||
109
browser-extension/src/entrypoints/content.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { createShadowRootUi } from 'wxt/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
cssInjectionMode: 'ui',
|
||||
allFrames: true,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
|
||||
/**
|
||||
* Main entry point for the content script.
|
||||
*/
|
||||
async main(ctx) {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a shadow root UI for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'aliasvault-ui',
|
||||
position: 'overlay',
|
||||
alignment: 'top-left',
|
||||
zIndex: 2147483646,
|
||||
anchor: 'html',
|
||||
/**
|
||||
* Handle mount.
|
||||
*/
|
||||
onMount(container) {
|
||||
/**
|
||||
* Handle input field focus.
|
||||
*/
|
||||
const handleFocusIn = async (e: FocusEvent) : Promise<void> => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if element itself, html or body has av-disable attribute like av-disable="true"
|
||||
const avDisable = ((e.target as HTMLElement).getAttribute('av-disable') ?? document.body?.getAttribute('av-disable') ?? document.documentElement.getAttribute('av-disable')) === 'true';
|
||||
if (avDisable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { isValid, inputElement } = validateInputField(e.target as Element);
|
||||
if (isValid && inputElement) {
|
||||
const formDetector = new FormDetector(document, inputElement);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(inputElement, container);
|
||||
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(inputElement, container);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for input field focus in the main document
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
|
||||
// Listen for popstate events (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingPopup(container);
|
||||
});
|
||||
|
||||
// Listen for messages from the background script
|
||||
onMessage('OPEN_AUTOFILL_POPUP', async (message: { data: { elementIdentifier: string } }) : Promise<messageBoolResponse> => {
|
||||
const { data } = message;
|
||||
const { elementIdentifier } = data;
|
||||
|
||||
if (!elementIdentifier) {
|
||||
return { success: false, error: 'No element identifier provided' };
|
||||
}
|
||||
|
||||
const target = document.getElementById(elementIdentifier) ?? document.getElementsByName(elementIdentifier)[0];
|
||||
const { isValid, inputElement } = validateInputField(target);
|
||||
|
||||
if (!isValid || !inputElement) {
|
||||
return { success: false, error: 'Target element is not a supported input field' };
|
||||
}
|
||||
|
||||
const formDetector = new FormDetector(document, inputElement);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return { success: false, error: 'No form found' };
|
||||
}
|
||||
|
||||
injectIcon(inputElement, container);
|
||||
openAutofillPopup(inputElement, container);
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Mount the UI to create the shadow root
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
108
browser-extension/src/entrypoints/contentScript/Filter.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { CombinedStopWords } from "@/utils/formDetector/FieldPatterns";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context to determine which credentials to show
|
||||
* in the autofill popup. Credentials are sorted by priority:
|
||||
* 1. Exact URL match (highest priority)
|
||||
* 2. Base URL match AND page title word match
|
||||
* 3. Base URL match only
|
||||
* 4. Page title word match only (lowest priority)
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
|
||||
const urlObject = new URL(currentUrl);
|
||||
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
|
||||
const sanitizedCurrentUrl = currentUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
// 1. Exact URL match (priority 1)
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedCredUrl = cred.ServiceUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
if (sanitizedCurrentUrl.startsWith(sanitizedCredUrl)) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
// If we have one or more exact matches, do not continue to other matches
|
||||
if (filtered.length > 0) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Prepare page title words for matching
|
||||
const titleWords = pageTitle.length > 0
|
||||
? pageTitle.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
|
||||
// Check for base URL matches and page title matches
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || filtered.some(f => f.Id === cred.Id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasBaseUrlMatch = false;
|
||||
let hasTitleMatch = false;
|
||||
|
||||
// Check base URL match
|
||||
try {
|
||||
const credUrlObject = new URL(cred.ServiceUrl);
|
||||
const currentUrlObject = new URL(baseUrl);
|
||||
|
||||
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
|
||||
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
|
||||
|
||||
const credRootDomain = credDomainParts.slice(-2).join('.');
|
||||
const currentRootDomain = currentDomainParts.slice(-2).join('.');
|
||||
|
||||
if (credUrlObject.protocol === currentUrlObject.protocol &&
|
||||
credRootDomain === currentRootDomain) {
|
||||
hasBaseUrlMatch = true;
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
|
||||
// Check page title match
|
||||
if (titleWords.length > 0) {
|
||||
const credNameWords = cred.ServiceName.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 3 && !CombinedStopWords.has(word));
|
||||
hasTitleMatch = titleWords.some(word =>
|
||||
credNameWords.some(credWord => credWord.includes(word))
|
||||
);
|
||||
}
|
||||
|
||||
// Assign priority based on matches
|
||||
if (hasBaseUrlMatch && hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 2 });
|
||||
} else if (hasBaseUrlMatch) {
|
||||
filtered.push({ ...cred, priority: 3 });
|
||||
} else if (hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 4 });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by priority and then take unique credentials
|
||||
const uniqueCredentials = Array.from(
|
||||
new Map(filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred]))
|
||||
.values()
|
||||
);
|
||||
// Show max 3 results
|
||||
return uniqueCredentials.slice(0, 3);
|
||||
}
|
||||
290
browser-extension/src/entrypoints/contentScript/Form.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { FormDetector } from "../../utils/formDetector/FormDetector";
|
||||
import { FormFiller } from "../../utils/formDetector/FormFiller";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
import { openAutofillPopup } from "./Popup";
|
||||
|
||||
/**
|
||||
* Global timestamp to track popup debounce time.
|
||||
* This is used to not show the popup again for a specific amount of time.
|
||||
* Used after autofill events to prevent spamming the popup from automatic
|
||||
* triggered browser events which can cause "focus" events to trigger.
|
||||
*/
|
||||
let popupDebounceTime = 0;
|
||||
|
||||
/**
|
||||
* Check if popup can be shown based on debounce time.
|
||||
*/
|
||||
export function popupDebounceTimeHasPassed() : boolean {
|
||||
if (Date.now() < popupDebounceTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide popup for a specific amount of time.
|
||||
*/
|
||||
export function hidePopupFor(ms: number) : void {
|
||||
popupDebounceTime = Date.now() + ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if an element is a supported input field that can be processed for autofill.
|
||||
* @param element The element to validate
|
||||
* @returns An object containing validation result and the element cast as HTMLInputElement if valid
|
||||
*/
|
||||
export function validateInputField(element: Element | null): { isValid: boolean; inputElement?: HTMLInputElement } {
|
||||
if (!element) {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url', 'number'];
|
||||
const elementType = element.getAttribute('type');
|
||||
const isInputElement = element.tagName.toLowerCase() === 'input';
|
||||
|
||||
// Check if it's a valid input field we should process
|
||||
const isValid = (
|
||||
// Case 1: It's an input element (with either explicit type or defaulting to "text")
|
||||
(isInputElement && (!elementType || textInputTypes.includes(elementType?.toLowerCase() ?? ''))) ||
|
||||
// Case 2: Non-input element but has valid type attribute
|
||||
(!isInputElement && elementType && textInputTypes.includes(elementType.toLowerCase()))
|
||||
) as boolean;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
inputElement: isValid ? (element as HTMLInputElement) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill credential into current form.
|
||||
*
|
||||
* @param credential - The credential to fill.
|
||||
* @param input - The input element that triggered the popup. Required when filling credentials to know which form to fill.
|
||||
*/
|
||||
export function fillCredential(credential: Credential, input: HTMLInputElement) : void {
|
||||
// Set debounce time to 300ms to prevent the popup from being shown again within 300ms because of autofill events.
|
||||
hidePopupFor(300);
|
||||
|
||||
const formDetector = new FormDetector(document, input);
|
||||
const form = formDetector.getForm();
|
||||
|
||||
if (!form) {
|
||||
// No form found, so we can't fill anything.
|
||||
return;
|
||||
}
|
||||
|
||||
const formFiller = new FormFiller(form, triggerInputEvents);
|
||||
formFiller.fillFields(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the actual visible input element, either the element itself or a child input.
|
||||
* Certain websites use custom input element wrappers that not only contain the input but
|
||||
* also other elements like labels, icons, etc. As we want to position the icon relative to the actual
|
||||
* input, we try to find the actual input element. If there is no actual input element, we fallback
|
||||
* to the provided element.
|
||||
*
|
||||
* This method is optional, but it improves the AliasVault icon positioning on certain websites.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
* @returns The actual input element to use for positioning.
|
||||
*/
|
||||
function findActualInput(element: HTMLElement): HTMLInputElement {
|
||||
// If it's already an input, return it
|
||||
if (element.tagName.toLowerCase() === 'input') {
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
// Try to find a visible child input
|
||||
const childInput = element.querySelector('input');
|
||||
if (childInput) {
|
||||
const style = window.getComputedStyle(childInput);
|
||||
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
||||
return childInput;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the provided element if no child input found
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
// Find the actual input element to use for positioning
|
||||
const actualInput = findActualInput(input);
|
||||
|
||||
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
<path d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z" fill="#EEC170"/>
|
||||
<path d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z" fill="#EEC170"/>
|
||||
<path d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z" fill="#EEC170"/>
|
||||
<path d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z" fill="#EEC170"/>
|
||||
</svg>`;
|
||||
|
||||
const ICON_HTML = `
|
||||
<div class="av-input-icon">
|
||||
<img src="data:image/svg+xml;base64,${btoa(aliasvaultIconSvg)}" style="width: 100%; height: 100%;" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Generate unique ID if input doesn't have one
|
||||
if (!actualInput.id) {
|
||||
actualInput.id = `aliasvault-input-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
// Create an overlay container at document level if it doesn't exist
|
||||
let overlayContainer = container.querySelector('#aliasvault-overlay-container');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div') as HTMLElement;
|
||||
overlayContainer.id = 'aliasvault-overlay-container';
|
||||
overlayContainer.className = 'av-overlay-container';
|
||||
container.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create the icon element from the HTML template
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.innerHTML = ICON_HTML;
|
||||
const icon = iconContainer.firstElementChild as HTMLElement;
|
||||
icon.setAttribute('data-icon-for', actualInput.id);
|
||||
|
||||
// Enable pointer events just for the icon
|
||||
icon.style.pointerEvents = 'auto';
|
||||
|
||||
/**
|
||||
* Update position of the icon.
|
||||
* Positions icon relative to right edge, moving it left by any existing padding.
|
||||
*/
|
||||
const updateIconPosition = () : void => {
|
||||
const rect = actualInput.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(actualInput);
|
||||
const paddingRight = parseInt(computedStyle.paddingLeft + computedStyle.paddingRight);
|
||||
|
||||
// Default offset is 32px, add any padding to move it further left
|
||||
const rightOffset = 24 + paddingRight;
|
||||
|
||||
icon.style.position = 'fixed';
|
||||
icon.style.top = `${rect.top + (rect.height - 24) / 2}px`;
|
||||
icon.style.left = `${(rect.left + rect.width) - rightOffset}px`;
|
||||
};
|
||||
|
||||
// Update position initially and on relevant events
|
||||
updateIconPosition();
|
||||
window.addEventListener('scroll', updateIconPosition, true);
|
||||
window.addEventListener('resize', updateIconPosition);
|
||||
|
||||
// Add click event to trigger the autofill popup and refocus the input
|
||||
icon.addEventListener('click', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => actualInput.focus(), 0);
|
||||
openAutofillPopup(actualInput, container);
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
overlayContainer.appendChild(icon);
|
||||
|
||||
// Fade in the icon
|
||||
requestAnimationFrame(() => {
|
||||
icon.style.opacity = '1';
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove the icon when the input loses focus.
|
||||
*/
|
||||
const handleBlur = (): void => {
|
||||
icon.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
icon.remove();
|
||||
actualInput.removeEventListener('blur', handleBlur);
|
||||
actualInput.removeEventListener('keydown', handleKeyPress);
|
||||
window.removeEventListener('scroll', updateIconPosition, true);
|
||||
window.removeEventListener('resize', updateIconPosition);
|
||||
|
||||
// Remove overlay container if it's empty
|
||||
if (!overlayContainer.children.length) {
|
||||
overlayContainer.remove();
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle key press to dismiss icon.
|
||||
*/
|
||||
const handleKeyPress = (e: KeyboardEvent): void => {
|
||||
// Dismiss on Enter, Escape, or Tab.
|
||||
if (e.key === 'Enter' || e.key === 'Escape' || e.key === 'Tab') {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
|
||||
actualInput.addEventListener('blur', handleBlur);
|
||||
actualInput.addEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger input events for an element to trigger form validation
|
||||
* which some websites require before the "continue" button is enabled.
|
||||
*/
|
||||
function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement, animate: boolean = true) : void {
|
||||
// Add keyframe animation if animation is requested
|
||||
if (animate) {
|
||||
// Create an overlay div that will show the highlight effect
|
||||
const overlay = document.createElement('div');
|
||||
|
||||
/**
|
||||
* Update position of the overlay.
|
||||
*/
|
||||
const updatePosition = () : void => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
background-color: rgba(244, 149, 65, 0.3);
|
||||
border-radius: ${getComputedStyle(element).borderRadius};
|
||||
animation: fadeOut 1.4s ease-out forwards;
|
||||
`;
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', updatePosition);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Remove overlay and cleanup after animation
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', updatePosition);
|
||||
overlay.remove();
|
||||
style.remove();
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
// Trigger events
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
if (element.type === 'radio') {
|
||||
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
||||
element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
1385
browser-extension/src/entrypoints/contentScript/Popup.ts
Normal file
790
browser-extension/src/entrypoints/contentScript/style.css
Normal file
@@ -0,0 +1,790 @@
|
||||
/* AliasVault Content Script Styles */
|
||||
body {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base Popup Styles */
|
||||
.av-popup {
|
||||
position: absolute;
|
||||
z-index: 2147483646;
|
||||
background-color: rgb(31, 41, 55);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 320px;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 4px;
|
||||
max-width: 90vw;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Loading Popup Styles */
|
||||
.av-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: av-loading-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes av-loading-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.av-loading-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Credential List Styles */
|
||||
.av-credential-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4b5563 #1f2937;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.av-credential-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.av-credential-item:hover {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.av-credential-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-grow: 1;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.av-credential-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.av-credential-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.av-service-name {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
color: #f3f4f6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-suggested-names {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #acacac;
|
||||
}
|
||||
.av-suggested-name {
|
||||
color: #bababa;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.av-suggested-name:hover {
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-service-details {
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #9ca3af;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-popout-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
margin-right: 16px;
|
||||
opacity: 0.6;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
color: #ffffff;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-popout-icon:hover {
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.av-no-matches {
|
||||
padding-left: 10px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.av-divider {
|
||||
height: 1px;
|
||||
background: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Action Container */
|
||||
.av-action-container {
|
||||
display: flex;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.av-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-button:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-button-primary {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-button-primary:hover {
|
||||
background-color: #d68338;
|
||||
}
|
||||
|
||||
.av-button-close {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.av-button-close:hover {
|
||||
background-color: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Search Input */
|
||||
.av-search-input {
|
||||
flex: 2;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
border: 1px solid #4b5563;
|
||||
outline: none;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.av-search-input::placeholder {
|
||||
color: #bdbebe;
|
||||
}
|
||||
|
||||
.av-search-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* Vault Locked Popup */
|
||||
.av-vault-locked {
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-vault-locked:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-vault-locked-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-vault-locked-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-vault-locked-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #d68338;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-vault-locked-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
.av-create-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.av-create-popup {
|
||||
position: relative;
|
||||
z-index: 1000000000;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
padding: 16px 24px;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-create-popup.show {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-help-text {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-modes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-btn:hover {
|
||||
background: #4b5563;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.av-create-popup-mode-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-icon .av-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.av-create-popup-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 6px;
|
||||
background: #374151;
|
||||
color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.av-create-popup-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.av-create-popup-input-default {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Custom Credential UI Styles */
|
||||
.av-create-popup-custom-toggle {
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.av-create-popup-toggle-text {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-custom-fields {
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-field-group {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-field-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #eee;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.av-create-popup-input-error {
|
||||
border-color: #ef4444 !important;
|
||||
box-shadow: 0 0 0 1px #ef4444 !important;
|
||||
}
|
||||
|
||||
.av-create-popup-error-text {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.av-create-popup-password-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-password-preview input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn,
|
||||
.av-create-popup-visibility-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 9px;
|
||||
background: #374151;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #e5e7eb;
|
||||
transition: background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn:hover,
|
||||
.av-create-popup-visibility-btn:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-visibility-btn .av-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.av-create-popup-error {
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.av-create-popup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.av-create-popup-back {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #374151;
|
||||
background: transparent;
|
||||
color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-back:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #374151;
|
||||
background: transparent;
|
||||
color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-save {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: #d68338;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-save:hover {
|
||||
background: #c97731;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* SVG Icons */
|
||||
.av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.av-icon-lock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Form Icon Styles */
|
||||
.av-overlay-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
}
|
||||
|
||||
.av-input-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Create Popup Styles */
|
||||
.av-create-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-create-popup-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #1f2937;
|
||||
border-left: 1px solid #374151;
|
||||
border-top: 1px solid #374151;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #374151;
|
||||
border-radius: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
112
browser-extension/src/entrypoints/popup/App.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { useAuth } from './context/AuthContext';
|
||||
import { useMinDurationLoading } from '../../hooks/useMinDurationLoading';
|
||||
import Header from './components/Layout/Header';
|
||||
import BottomNav from './components/Layout/BottomNav';
|
||||
import AuthSettings from './pages/AuthSettings';
|
||||
import CredentialsList from './pages/CredentialsList';
|
||||
import EmailsList from './pages/EmailsList';
|
||||
import LoadingSpinner from './components/LoadingSpinner';
|
||||
import Home from './pages/Home';
|
||||
import CredentialDetails from './pages/CredentialDetails';
|
||||
import EmailDetails from './pages/EmailDetails';
|
||||
import Settings from './pages/Settings';
|
||||
import GlobalStateChangeHandler from './components/GlobalStateChangeHandler';
|
||||
import { useLoading } from './context/LoadingContext';
|
||||
import Logout from './pages/Logout';
|
||||
import './style.css';
|
||||
|
||||
/**
|
||||
* Route configuration.
|
||||
*/
|
||||
type RouteConfig = {
|
||||
path: string;
|
||||
element: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* App component.
|
||||
*/
|
||||
const App: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const { isInitialLoading } = useLoading();
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
// Add these route configurations
|
||||
const routes: RouteConfig[] = [
|
||||
{ path: '/', element: <Home />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: 'Credential details' },
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
{ path: '/logout', element: <Logout />, showBackButton: false },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isInitialLoading, setIsLoading]);
|
||||
|
||||
/**
|
||||
* Print global message if it exists.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (authContext.globalMessage) {
|
||||
setMessage(authContext.globalMessage);
|
||||
} else {
|
||||
setMessage(null);
|
||||
}
|
||||
}, [authContext, authContext.globalMessage]);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col">
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GlobalStateChangeHandler />
|
||||
<Header
|
||||
routes={routes}
|
||||
/>
|
||||
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100% - 120px)',
|
||||
maxHeight: '600px',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
type ButtonProps = {
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'primary' | 'secondary';
|
||||
};
|
||||
|
||||
/**
|
||||
* Button component
|
||||
*/
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
onClick,
|
||||
children,
|
||||
type = 'button',
|
||||
variant = 'primary'
|
||||
}) => {
|
||||
const colorClasses = {
|
||||
primary: 'bg-primary-500 hover:bg-primary-600',
|
||||
secondary: 'bg-gray-500 hover:bg-gray-600'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${colorClasses[variant]} text-white font-medium rounded-lg px-4 py-2 text-sm w-full`}
|
||||
onClick={onClick}
|
||||
type={type}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,172 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { MailboxEmail } from '../../../utils/types/webapi/MailboxEmail';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
type EmailPreviewProps = {
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a preview of the latest emails in the inbox.
|
||||
*/
|
||||
export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
const [emails, setEmails] = useState<MailboxEmail[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastEmailId, setLastEmailId] = useState<number>(0);
|
||||
const [isSpamOk, setIsSpamOk] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Checks if the email is a public domain.
|
||||
*/
|
||||
const isPublicDomain = async (emailAddress: string): Promise<boolean> => {
|
||||
// Get metadata from storage
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[] ?? [];
|
||||
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the latest emails from the server and decrypts them locally if needed.
|
||||
*/
|
||||
const loadEmails = async (): Promise<void> => {
|
||||
try {
|
||||
const isPublic = await isPublicDomain(email);
|
||||
setIsSpamOk(isPublic);
|
||||
|
||||
if (isPublic) {
|
||||
// For public domains (SpamOK), use the SpamOK API directly
|
||||
const emailPrefix = email.split('@')[0];
|
||||
const response = await fetch(`https://api.spamok.com/v2/EmailBox/${emailPrefix}`, {
|
||||
headers: {
|
||||
'X-Asdasd-Platform-Id': 'av-chrome',
|
||||
'X-Asdasd-Platform-Version': AppInfo.VERSION,
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data?.mails
|
||||
?.toSorted((a: MailboxEmail, b: MailboxEmail) =>
|
||||
new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
?.slice(0, 2) ?? [];
|
||||
|
||||
if (loading && latestMails.length > 0) {
|
||||
setLastEmailId(latestMails[0].id);
|
||||
}
|
||||
|
||||
setEmails(latestMails);
|
||||
} else {
|
||||
// For private domains, use existing encrypted email logic
|
||||
const response = await webApi.get(`EmailBox/${email}`);
|
||||
const data = response as { mails: MailboxEmail[] };
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data.mails
|
||||
.toSorted((a, b) => new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
.slice(0, 2);
|
||||
|
||||
if (latestMails) {
|
||||
// Loop through all emails and decrypt them locally
|
||||
const decryptedEmails: MailboxEmail[] = await EncryptionUtility.decryptEmailList(
|
||||
latestMails,
|
||||
dbContext.sqliteClient!.getAllEncryptionKeys()
|
||||
);
|
||||
|
||||
if (loading && decryptedEmails.length > 0) {
|
||||
setLastEmailId(decryptedEmails[0].id);
|
||||
}
|
||||
|
||||
setEmails(decryptedEmails);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading emails:', err);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
loadEmails();
|
||||
// Set up auto-refresh interval
|
||||
const interval = setInterval(loadEmails, 2000);
|
||||
return () : void => clearInterval(interval);
|
||||
}, [email, loading, webApi, dbContext]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
Loading emails...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (emails.length === 0) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
No emails received yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
|
||||
{emails.map((mail) => (
|
||||
isSpamOk ? (
|
||||
<a
|
||||
key={mail.id}
|
||||
href={`https://spamok.com/${email.split('@')[0]}/${mail.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="truncate flex-1">
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{mail.subject.substring(0, 30)}{mail.subject.length > 30 ? '...' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
{new Date(mail.dateSystem).toLocaleDateString()}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
key={mail.id}
|
||||
to={`/emails/${mail.id}`}
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="truncate flex-1">
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{mail.subject.substring(0, 30)}{mail.subject.length > 30 ? '...' : ''}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2">
|
||||
{new Date(mail.dateSystem).toLocaleDateString()}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ClipboardCopyService } from '../utils/ClipboardCopyService';
|
||||
|
||||
/**
|
||||
* Form input copy to clipboard props.
|
||||
*/
|
||||
type FormInputCopyToClipboardProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
type?: 'text' | 'password';
|
||||
}
|
||||
|
||||
const clipboardService = new ClipboardCopyService();
|
||||
|
||||
/**
|
||||
* Form input copy to clipboard component.
|
||||
*/
|
||||
export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
type = 'text'
|
||||
}) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = clipboardService.subscribe((copiedId) : void => {
|
||||
setCopied(copiedId === id);
|
||||
});
|
||||
return () : void => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
* Copy to clipboard.
|
||||
*/
|
||||
const copyToClipboard = async () : Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
clipboardService.setCopied(id);
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
if (clipboardService.getCopiedId() === id) {
|
||||
clipboardService.setCopied('');
|
||||
}
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id} className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={type === 'password' && !showPassword ? 'password' : 'text'}
|
||||
id={id}
|
||||
readOnly
|
||||
value={value}
|
||||
onClick={copyToClipboard}
|
||||
className={`w-full px-3 py-2.5 bg-white border ${
|
||||
copied ? 'border-green-500 border-2' : 'border-gray-300'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{copied && (
|
||||
<span className="text-green-500 dark:text-green-400">
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
{type === 'password' && (
|
||||
<button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
/**
|
||||
* Global state change handler component which listens for global state changes and e.g. redirects user to login
|
||||
* page if login state changes.
|
||||
*/
|
||||
const GlobalStateChangeHandler: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const lastLoginState = useRef(authContext.isLoggedIn);
|
||||
const initialRender = useRef(true);
|
||||
|
||||
/**
|
||||
* Listen for auth logged in changes and redirect to home page if logged in state changes to handle logins and logouts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Only navigate when auth state is different from the last state we acted on.
|
||||
if (lastLoginState.current !== authContext.isLoggedIn) {
|
||||
lastLoginState.current = authContext.isLoggedIn;
|
||||
|
||||
/**
|
||||
* Skip the first auth state change to avoid redirecting when popup opens for the first time
|
||||
* which already causes the auth state to change from false to true.
|
||||
*/
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to home page if logged in state changes.
|
||||
navigate('/');
|
||||
}
|
||||
}, [authContext.isLoggedIn]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default GlobalStateChangeHandler;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useDb } from '../../context/DbContext';
|
||||
|
||||
type TabName = 'credentials' | 'emails' | 'settings';
|
||||
|
||||
/**
|
||||
* Bottom nav component.
|
||||
*/
|
||||
const BottomNav: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('credentials');
|
||||
|
||||
// Add effect to update currentTab based on route
|
||||
useEffect(() => {
|
||||
const path = location.pathname.substring(1) as TabName;
|
||||
if (['credentials', 'emails', 'settings'].includes(path)) {
|
||||
setCurrentTab(path);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
/**
|
||||
* Handle tab change.
|
||||
*/
|
||||
const handleTabChange = (tab: TabName) : void => {
|
||||
setCurrentTab(tab);
|
||||
navigate(`/${tab}`);
|
||||
};
|
||||
|
||||
if (!authContext.isLoggedIn || !dbContext.dbAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detect if the user is coming from the unlock page with mode=inline_unlock.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isInlineUnlockMode = urlParams.get('mode') === 'inline_unlock';
|
||||
|
||||
if (isInlineUnlockMode) {
|
||||
// Do not show the bottom nav for inline unlock mode.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-around items-center h-14">
|
||||
<button
|
||||
onClick={() => handleTabChange('credentials')}
|
||||
className={`flex flex-col items-center justify-center w-1/3 h-full ${
|
||||
currentTab === 'credentials' ? 'text-primary-600 dark:text-primary-500' : 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Credentials</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('emails')}
|
||||
className={`flex flex-col items-center justify-center w-1/3 h-full ${
|
||||
currentTab === 'emails' ? 'text-primary-600 dark:text-primary-500' : 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Emails</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('settings')}
|
||||
className={`flex flex-col items-center justify-center w-1/3 h-full ${
|
||||
currentTab === 'settings' ? 'text-primary-600 dark:text-primary-500' : 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomNav;
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { UserMenu } from './UserMenu';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { AppInfo } from '../../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Header props.
|
||||
*/
|
||||
type HeaderProps = {
|
||||
routes?: {
|
||||
path: string;
|
||||
showBackButton?: boolean;
|
||||
title?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Header component.
|
||||
*/
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
routes = []
|
||||
}) => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Updated route matching logic to handle URL parameters
|
||||
const currentRoute = routes?.find(route => {
|
||||
// Convert route pattern to regex
|
||||
const pattern = route.path.replace(/:\w+/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
return regex.test(location.pathname);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle settings.
|
||||
*/
|
||||
const handleSettings = () : void => {
|
||||
navigate('/auth-settings');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logo click.
|
||||
*/
|
||||
const logoClick = () : void => {
|
||||
// If logged in, navigate to credentials.
|
||||
if (authContext.isLoggedIn) {
|
||||
navigate('/credentials');
|
||||
} else {
|
||||
// If not logged in, navigate to index.
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="flex items-center h-16 px-4">
|
||||
{currentRoute?.showBackButton ? (
|
||||
<button
|
||||
id="back"
|
||||
onClick={() => navigate(-1)}
|
||||
className="flex items-center gap-2 hover:bg-gray-100 dark:hover:bg-gray-700 pr-2 pt-1.5 pb-1.5 rounded-lg group"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 text-gray-500 group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{currentRoute.title && (
|
||||
<h1 className="text-lg font-medium text-gray-900 dark:text-white ml-2">
|
||||
{currentRoute.title}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => logoClick()}
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-grow" />
|
||||
|
||||
<div className="flex items-center">
|
||||
{!currentRoute?.showBackButton ? (
|
||||
<button
|
||||
onClick={openClientTab}
|
||||
className="p-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (<></>)}
|
||||
</div>
|
||||
{!authContext.isLoggedIn ? (
|
||||
<button
|
||||
id="settings"
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="sr-only">Settings</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<UserMenu />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../../context/LoadingContext';
|
||||
|
||||
/**
|
||||
* User menu component.
|
||||
*/
|
||||
export const UserMenu: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Handle clicking outside the user menu.
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent) : void => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
buttonRef.current &&
|
||||
!menuRef.current.contains(event.target as Node) &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () : void => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Toggle the user menu.
|
||||
*/
|
||||
const toggleUserMenu = () : void => {
|
||||
setIsUserMenuOpen(!isUserMenuOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logging out.
|
||||
*/
|
||||
const onLogout = async () : Promise<void> => {
|
||||
showLoading();
|
||||
navigate('/logout', { replace: true });
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={toggleUserMenu}
|
||||
className="flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<svg className="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isUserMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute right-0 z-50 mt-2 w-48 py-1 bg-white rounded-lg shadow-lg dark:bg-gray-700 border border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-600">
|
||||
<span className="block text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:text-red-400 dark:hover:bg-gray-600"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenu;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Loading spinner component used throughout the app for showing a loading spinner
|
||||
* inline in the page.
|
||||
*/
|
||||
const LoadingSpinner: React.FC = () => {
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
};
|
||||
|
||||
const spinner = (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
className="border-[4px] border-solid border-current/10 dark:border-white/10 border-t-current dark:border-t-white"
|
||||
style={spinnerStyle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center">
|
||||
{spinner}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
/**
|
||||
* Loading spinner full screen component used throughout the app for showing a loading spinner
|
||||
* that covers the entire screen.
|
||||
*/
|
||||
const LoadingSpinnerFullScreen: React.FC = () => {
|
||||
const { isLoading } = useLoading();
|
||||
|
||||
if (!isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
};
|
||||
|
||||
const spinner = (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
className="border-[4px] border-solid border-current/10 dark:border-white/10 border-t-current dark:border-t-white"
|
||||
style={spinnerStyle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 w-full h-full z-50 bg-gray-200 dark:bg-gray-500 bg-opacity-90 flex items-center justify-center">
|
||||
<div className="relative">
|
||||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingSpinnerFullScreen;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Component for displaying the login server information.
|
||||
*/
|
||||
const LoginServerInfo: React.FC = () => {
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the base URL for the login server.
|
||||
*/
|
||||
const loadApiUrl = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
setBaseUrl(apiUrl ?? AppInfo.DEFAULT_API_URL);
|
||||
};
|
||||
loadApiUrl();
|
||||
}, []);
|
||||
|
||||
const isDefaultServer = !baseUrl || baseUrl === AppInfo.DEFAULT_API_URL;
|
||||
const displayUrl = isDefaultServer ? 'aliasvault.net' : new URL(baseUrl).hostname;
|
||||
|
||||
/**
|
||||
* Handles the click event for the login server information.
|
||||
*/
|
||||
const handleClick = () : void => {
|
||||
navigate('/auth-settings');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||||
(Connecting to{' '}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500 underline"
|
||||
>
|
||||
{displayUrl}
|
||||
</button>)
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginServerInfo;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Reload button props.
|
||||
*/
|
||||
type ReloadButtonProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reload button component.
|
||||
*/
|
||||
const ReloadButton: React.FC<ReloadButtonProps> = ({ onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className="px-2 items-center"
|
||||
>
|
||||
<div className="relative inline-flex items-center">
|
||||
<button onClick={onClick} className="absolute p-2 hover:bg-gray-200 rounded-2xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg aria-hidden="true" className="inline w-8 h-8 text-gray-200 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>
|
||||
</div>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReloadButton;
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { TotpCode } from '../../../utils/types/TotpCode';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
|
||||
type TotpViewerProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows TOTP codes for a credential.
|
||||
*/
|
||||
export const TotpViewer: React.FC<TotpViewerProps> = ({ credentialId }) => {
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentCodes, setCurrentCodes] = useState<Record<string, string>>({});
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Gets the remaining seconds for the TOTP code.
|
||||
*/
|
||||
const getRemainingSeconds = (step = 30): number => {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: 'dummy', // We only need this for timing calculations
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: step
|
||||
});
|
||||
return totp.period - (Math.floor(Date.now() / 1000) % totp.period);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the remaining percentage for the TOTP code.
|
||||
*/
|
||||
const getRemainingPercentage = (): number => {
|
||||
const remaining = getRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return Math.floor(((30.0 - remaining) / 30.0) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a TOTP code for a given secret key.
|
||||
*/
|
||||
const generateTotpCode = (secretKey: string): string => {
|
||||
try {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: secretKey,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30
|
||||
});
|
||||
return totp.generate();
|
||||
} catch (error) {
|
||||
console.error('Error generating TOTP code:', error);
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies a TOTP code to the clipboard.
|
||||
*/
|
||||
const copyToClipboard = async (code: string, id: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopiedId(id);
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the TOTP codes for the credential.
|
||||
*/
|
||||
const loadTotpCodes = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const codes = dbContext.sqliteClient.getTotpCodesForCredential(credentialId);
|
||||
setTotpCodes(codes);
|
||||
} catch (error) {
|
||||
console.error('Error loading TOTP codes:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTotpCodes();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Updates the current TOTP codes.
|
||||
*/
|
||||
const updateTotpCodes = (prevCodes: Record<string, string>): Record<string, string> => {
|
||||
const newCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
const generatedCode = generateTotpCode(code.SecretKey);
|
||||
// Only update if we have a valid code
|
||||
if (generatedCode !== 'Error') {
|
||||
newCodes[code.Id] = generatedCode;
|
||||
} else {
|
||||
// Keep the previous code if there's an error
|
||||
newCodes[code.Id] = prevCodes[code.Id] ?? 'Error';
|
||||
}
|
||||
});
|
||||
return newCodes;
|
||||
};
|
||||
|
||||
// Generate initial codes
|
||||
const initialCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
initialCodes[code.Id] = generateTotpCode(code.SecretKey);
|
||||
});
|
||||
setCurrentCodes(initialCodes);
|
||||
|
||||
// Set up interval to refresh codes
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentCodes(updateTotpCodes);
|
||||
}, 1000);
|
||||
|
||||
// Clean up interval on unmount or when totpCodes change
|
||||
return () : void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [totpCodes]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Two-factor authentication</h2>
|
||||
Loading TOTP codes...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (totpCodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-white">Two-factor authentication</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{totpCodes.map(totpCode => (
|
||||
<button
|
||||
key={totpCode.Id}
|
||||
className={`w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700`}
|
||||
onClick={() => copyToClipboard(currentCodes[totpCode.Id], totpCode.Id)}
|
||||
aria-label={`Copy ${totpCode.Name} code`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{totpCode.Name}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{currentCodes[totpCode.Id]}
|
||||
</span>
|
||||
<div className="text-xs">
|
||||
{copiedId === totpCode.Id ? (
|
||||
<span className="text-green-600 dark:text-green-400">Copied!</span>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">{getRemainingSeconds()}s</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1 h-6 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div
|
||||
className="bg-blue-600 rounded-full transition-all"
|
||||
style={{ height: `${getRemainingPercentage()}%`, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
126
browser-extension/src/entrypoints/popup/context/AuthContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useDb } from './DbContext';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
isInitialized: boolean;
|
||||
username: string | null;
|
||||
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
|
||||
login: () => Promise<void>;
|
||||
logout: (errorMessage?: string) => Promise<void>;
|
||||
globalMessage: string | null;
|
||||
clearGlobalMessage: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth context.
|
||||
*/
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* AuthProvider to provide the authentication state to the app that components can use.
|
||||
*/
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [globalMessage, setGlobalMessage] = useState<string | null>(null);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Check for tokens in chrome storage on initial load.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Initialize the authentication state.
|
||||
*/
|
||||
const initializeAuth = async () : Promise<void> => {
|
||||
const accessToken = await storage.getItem('local:accessToken') as string;
|
||||
const refreshToken = await storage.getItem('local:refreshToken') as string;
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set auth tokens in chrome storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
*/
|
||||
const setAuthTokens = useCallback(async (username: string, accessToken: string, refreshToken: string) : Promise<void> => {
|
||||
await storage.setItem('local:username', username);
|
||||
await storage.setItem('local:accessToken', accessToken);
|
||||
await storage.setItem('local:refreshToken', refreshToken);
|
||||
|
||||
setUsername(username);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set logged in status to true which refreshes the app.
|
||||
*/
|
||||
const login = useCallback(async () : Promise<void> => {
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Logout the user and clear the auth tokens from chrome storage.
|
||||
*/
|
||||
const logout = useCallback(async (errorMessage?: string) : Promise<void> => {
|
||||
await sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
await storage.removeItems(['local:username', 'local:accessToken', 'local:refreshToken']);
|
||||
dbContext?.clearDatabase();
|
||||
|
||||
// Set local storage global message that will be shown on the login page.
|
||||
if (errorMessage) {
|
||||
setGlobalMessage(errorMessage);
|
||||
}
|
||||
|
||||
setUsername(null);
|
||||
setIsLoggedIn(false);
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Clear global message (called after displaying the message).
|
||||
*/
|
||||
const clearGlobalMessage = useCallback(() : void => {
|
||||
setGlobalMessage(null);
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
isLoggedIn,
|
||||
isInitialized,
|
||||
username,
|
||||
setAuthTokens,
|
||||
login,
|
||||
logout,
|
||||
globalMessage,
|
||||
clearGlobalMessage,
|
||||
}), [isLoggedIn, isInitialized, username, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the AuthContext
|
||||
*/
|
||||
export const useAuth = () : AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
151
browser-extension/src/entrypoints/popup/context/DbContext.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { VaultResponse as messageVaultResponse } from '../../../utils/types/messaging/VaultResponse';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
dbInitialized: boolean;
|
||||
dbAvailable: boolean;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<void>;
|
||||
clearDatabase: () => void;
|
||||
vaultRevision: number;
|
||||
publicEmailDomains: string[];
|
||||
privateEmailDomains: string[];
|
||||
}
|
||||
|
||||
const DbContext = createContext<DbContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* DbProvider to provide the SQLite client to the app that components can use to make database queries.
|
||||
*/
|
||||
export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* SQLite client.
|
||||
*/
|
||||
const [sqliteClient, setSqliteClient] = useState<SqliteClient | null>(null);
|
||||
|
||||
/**
|
||||
* Database initialization state. If true, the database has been initialized and the dbAvailable state is correct.
|
||||
*/
|
||||
const [dbInitialized, setDbInitialized] = useState(false);
|
||||
|
||||
/**
|
||||
* Database availability state. If true, the database is available. If false, the database is not available and needs to be unlocked or retrieved again from the API.
|
||||
*/
|
||||
const [dbAvailable, setDbAvailable] = useState(false);
|
||||
|
||||
/**
|
||||
* Public email domains.
|
||||
*/
|
||||
const [publicEmailDomains, setPublicEmailDomains] = useState<string[]>([]);
|
||||
|
||||
/**
|
||||
* Vault revision.
|
||||
*/
|
||||
const [vaultRevision, setVaultRevision] = useState(0);
|
||||
|
||||
/**
|
||||
* Private email domains.
|
||||
*/
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
|
||||
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
|
||||
// Attempt to decrypt the blob.
|
||||
const decryptedBlob = await EncryptionUtility.symmetricDecrypt(
|
||||
vaultResponse.vault.blob,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client.
|
||||
const client = new SqliteClient();
|
||||
await client.initializeFromBase64(decryptedBlob);
|
||||
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(vaultResponse.vault.publicEmailDomainList);
|
||||
setPrivateEmailDomains(vaultResponse.vault.privateEmailDomainList);
|
||||
setVaultRevision(vaultResponse.vault.currentRevisionNumber);
|
||||
|
||||
/*
|
||||
* Store encrypted vault in background worker.
|
||||
*/
|
||||
sendMessage('STORE_VAULT', {
|
||||
derivedKey: derivedKey,
|
||||
vaultResponse: vaultResponse,
|
||||
}, 'background');
|
||||
}, []);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
try {
|
||||
const response = await sendMessage('GET_VAULT', {}, 'background') as messageVaultResponse;
|
||||
if (response?.vault) {
|
||||
const client = new SqliteClient();
|
||||
await client.initializeFromBase64(response.vault);
|
||||
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(response.publicEmailDomains ?? []);
|
||||
setPrivateEmailDomains(response.privateEmailDomains ?? []);
|
||||
setVaultRevision(response.vaultRevisionNumber ?? 0);
|
||||
} else {
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrieving vault from background:', error);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if database is initialized and try to retrieve vault from background
|
||||
*/
|
||||
useEffect(() : void => {
|
||||
if (!dbInitialized) {
|
||||
checkStoredVault();
|
||||
}
|
||||
}, [dbInitialized, checkStoredVault]);
|
||||
|
||||
/**
|
||||
* Clear database and remove from background worker, called when logging out.
|
||||
*/
|
||||
const clearDatabase = useCallback(() : void => {
|
||||
setSqliteClient(null);
|
||||
setDbInitialized(false);
|
||||
sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
sqliteClient,
|
||||
dbInitialized,
|
||||
dbAvailable,
|
||||
initializeDatabase,
|
||||
clearDatabase,
|
||||
vaultRevision,
|
||||
publicEmailDomains,
|
||||
privateEmailDomains
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, vaultRevision, publicEmailDomains, privateEmailDomains]);
|
||||
|
||||
return (
|
||||
<DbContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</DbContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the DbContext
|
||||
*/
|
||||
export const useDb = () : DbContextType => {
|
||||
const context = useContext(DbContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useDb must be used within a DbProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { createContext, useContext, useState, useMemo } from 'react';
|
||||
import LoadingSpinnerFullScreen from '../components/LoadingSpinnerFullScreen';
|
||||
|
||||
type LoadingContextType = {
|
||||
isLoading: boolean;
|
||||
showLoading: () => void;
|
||||
hideLoading: () => void;
|
||||
isInitialLoading: boolean;
|
||||
setIsInitialLoading: (isInitialLoading: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading context.
|
||||
*/
|
||||
const LoadingContext = createContext<LoadingContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Loading provider
|
||||
*/
|
||||
export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* Initial loading state for when extension is first loaded. This initial loading state is
|
||||
* hidden by the component that is rendered when the extension is first loaded to prevent
|
||||
* multiple loading spinners from being shown.
|
||||
*/
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
|
||||
/**
|
||||
* Loading state that can be used by other components during normal operation.
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
*/
|
||||
const showLoading = (): void => setIsLoading(true);
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
*/
|
||||
const hideLoading = (): void => setIsLoading(false);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isLoading,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
isInitialLoading,
|
||||
setIsInitialLoading,
|
||||
}),
|
||||
[isLoading, isInitialLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={value}>
|
||||
<LoadingSpinnerFullScreen />
|
||||
{children}
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use loading state
|
||||
*/
|
||||
export const useLoading = (): LoadingContextType => {
|
||||
const context = useContext(LoadingContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLoading must be used within a LoadingProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
134
browser-extension/src/entrypoints/popup/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Theme type.
|
||||
*/
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
/**
|
||||
* Theme preference key in storage.
|
||||
*/
|
||||
const THEME_PREFERENCE_KEY = 'local:theme';
|
||||
|
||||
/**
|
||||
* Theme context type.
|
||||
*/
|
||||
type ThemeContextType = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme context.
|
||||
*/
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Theme provider
|
||||
*/
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* Theme state that can be 'light', 'dark', or 'system'.
|
||||
*/
|
||||
const [theme, setTheme] = useState<Theme>('system');
|
||||
|
||||
/**
|
||||
* Tracks whether dark mode is active (based on theme or system preference).
|
||||
*/
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load theme setting from storage.
|
||||
*/
|
||||
const loadTheme = async () : Promise<void> => {
|
||||
const savedTheme = await getTheme();
|
||||
setTheme(savedTheme);
|
||||
};
|
||||
loadTheme();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set the theme and save to storage.
|
||||
*/
|
||||
const updateTheme = useCallback((newTheme: Theme): void => {
|
||||
setTheme(newTheme);
|
||||
setStoredTheme(newTheme);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the theme from storage.
|
||||
*/
|
||||
const getTheme = async (): Promise<Theme> => {
|
||||
return (await storage.getItem(THEME_PREFERENCE_KEY) as Theme) || 'system';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the theme in storage.
|
||||
*/
|
||||
const setStoredTheme = async (theme: Theme): Promise<void> => {
|
||||
await storage.setItem(THEME_PREFERENCE_KEY, theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect to apply theme to document and handle system preference changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Update the dark mode status.
|
||||
*/
|
||||
const updateDarkMode = (): void => {
|
||||
if (theme === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setIsDarkMode(prefersDark);
|
||||
document.documentElement.classList.toggle('dark', prefersDark);
|
||||
} else {
|
||||
const isDark = theme === 'dark';
|
||||
setIsDarkMode(isDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system preference changes if using 'system' theme
|
||||
if (theme === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
/**
|
||||
* Update the dark mode status when the system preference changes.
|
||||
*/
|
||||
const handler = () : void => updateDarkMode();
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () : void => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme: updateTheme,
|
||||
isDarkMode,
|
||||
}),
|
||||
[theme, isDarkMode, updateTheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use theme state
|
||||
*/
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { WebApiService } from '../../../utils/WebApiService';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
const WebApiContext = createContext<WebApiService | null>(null);
|
||||
|
||||
/**
|
||||
* WebApiProvider to provide the WebApiService to the app that components can use.
|
||||
*/
|
||||
export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { logout } = useAuth();
|
||||
const [webApiService, setWebApiService] = useState<WebApiService | null>(null);
|
||||
|
||||
/**
|
||||
* Initialize WebApiService
|
||||
*/
|
||||
useEffect(() : void => {
|
||||
const service = new WebApiService(
|
||||
(statusError: string | null) => {
|
||||
if (statusError) {
|
||||
logout(statusError);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
);
|
||||
setWebApiService(service);
|
||||
}, [logout]);
|
||||
|
||||
if (!webApiService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WebApiContext.Provider value={webApiService}>
|
||||
{children}
|
||||
</WebApiContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the WebApiService
|
||||
*/
|
||||
export const useWebApi = () : WebApiService => {
|
||||
const context = useContext(WebApiContext);
|
||||
if (!context) {
|
||||
throw new Error('useWebApi must be used within a WebApiProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
14
browser-extension/src/entrypoints/popup/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault</title>
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
browser-extension/src/entrypoints/popup/main.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import { WebApiProvider } from './context/WebApiContext';
|
||||
import { DbProvider } from './context/DbContext';
|
||||
import { LoadingProvider } from './context/LoadingContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { setupExpandedMode } from '../../utils/ExpandedMode';
|
||||
|
||||
// Run before React initializes to ensure the popup is always a fixed width except for when explicitly expanded.
|
||||
setupExpandedMode();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
);
|
||||
179
browser-extension/src/entrypoints/popup/pages/AuthSettings.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS: ApiOption[] = [
|
||||
{ label: 'Aliasvault.net', value: AppInfo.DEFAULT_API_URL },
|
||||
{ label: 'Self-hosted', value: 'custom' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Auth settings page only shown when user is not logged in.
|
||||
*/
|
||||
const AuthSettings: React.FC = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load the stored settings from the storage.
|
||||
*/
|
||||
const loadStoredSettings = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
const clientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
|
||||
if (dismissUntil) {
|
||||
setIsGloballyEnabled(false);
|
||||
} else {
|
||||
setIsGloballyEnabled(globallyEnabled);
|
||||
}
|
||||
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
|
||||
|
||||
if (matchingOption) {
|
||||
setSelectedOption(matchingOption.value);
|
||||
} else if (apiUrl) {
|
||||
setSelectedOption('custom');
|
||||
setCustomUrl(apiUrl);
|
||||
setCustomClientUrl(clientUrl ?? '');
|
||||
} else {
|
||||
setSelectedOption(DEFAULT_OPTIONS[0].value);
|
||||
}
|
||||
};
|
||||
|
||||
loadStoredSettings();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle option change
|
||||
*/
|
||||
const handleOptionChange = async (e: React.ChangeEvent<HTMLSelectElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setSelectedOption(value);
|
||||
if (value !== 'custom') {
|
||||
await storage.setItem('local:apiUrl', '');
|
||||
await storage.setItem('local:clientUrl', '');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle custom API URL change
|
||||
*/
|
||||
const handleCustomUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomUrl(value);
|
||||
await storage.setItem('local:apiUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle custom client URL change
|
||||
* @param e
|
||||
*/
|
||||
const handleCustomClientUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomClientUrl(value);
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
if (newGloballyEnabled) {
|
||||
// Reset all disabled sites when enabling globally
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}
|
||||
|
||||
setIsGloballyEnabled(newGloballyEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-6">
|
||||
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
API Connection
|
||||
</label>
|
||||
<select
|
||||
value={selectedOption}
|
||||
onChange={handleOptionChange}
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
>
|
||||
{DEFAULT_OPTIONS.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedOption === 'custom' && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-client-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom client URL
|
||||
</label>
|
||||
<input
|
||||
id="custom-client-url"
|
||||
type="text"
|
||||
value={customClientUrl}
|
||||
onChange={handleCustomClientUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com"
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom API URL
|
||||
</label>
|
||||
<input
|
||||
id="custom-api-url"
|
||||
type="text"
|
||||
value={customUrl}
|
||||
onChange={handleCustomUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com/api"
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isGloballyEnabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthSettings;
|
||||
@@ -0,0 +1,329 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { Credential } from '../../../utils/types/Credential';
|
||||
import { FormInputCopyToClipboard } from '../components/FormInputCopyToClipboard';
|
||||
import { EmailPreview } from '../components/EmailPreview';
|
||||
import { TotpViewer } from '../components/TotpViewer';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
|
||||
type BlockProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block.
|
||||
*/
|
||||
const Block: React.FC<BlockProps> = ({ children, className = '' }) => (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the header block.
|
||||
*/
|
||||
const HeaderBlock: React.FC<{ credential: Credential; onOpenNewPopup: () => void }> = ({ credential, onOpenNewPopup }) => (
|
||||
<Block className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={SqliteClient.imgSrcFromBytes(credential.Logo)}
|
||||
alt={credential.ServiceName}
|
||||
className="w-12 h-12 rounded-lg mr-4"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{credential.ServiceName}</h1>
|
||||
{credential.ServiceUrl && (
|
||||
<a
|
||||
href={credential.ServiceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenNewPopup}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
title="Open in new window"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the email block.
|
||||
*/
|
||||
const EmailBlock: React.FC<{ email: string; isSupported: boolean }> = ({ email, isSupported }) => (
|
||||
<Block>
|
||||
{isSupported && <EmailPreview email={email} />}
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the TOTP viewer block.
|
||||
*/
|
||||
const TotpBlock: React.FC<{ credentialId: string }> = ({ credentialId }) => (
|
||||
<Block>
|
||||
<TotpViewer credentialId={credentialId} />
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the login credentials block.
|
||||
*/
|
||||
const LoginCredentialsBlock: React.FC<{ credential: Credential }> = ({ credential }) => {
|
||||
const email = credential.Alias?.Email?.trim();
|
||||
const username = credential.Username?.trim();
|
||||
const password = credential.Password?.trim();
|
||||
|
||||
if (!email && !username && !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Login credentials</h2>
|
||||
{email && (
|
||||
<FormInputCopyToClipboard
|
||||
id="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
/>
|
||||
)}
|
||||
{username && (
|
||||
<FormInputCopyToClipboard
|
||||
id="username"
|
||||
label="Username"
|
||||
value={username}
|
||||
/>
|
||||
)}
|
||||
{password && (
|
||||
<FormInputCopyToClipboard
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
type="password"
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the alias block.
|
||||
*/
|
||||
const AliasBlock: React.FC<{ credential: Credential; isValidDate: (date: string | null | undefined) => boolean }> = ({
|
||||
credential,
|
||||
isValidDate
|
||||
}) => {
|
||||
const hasFirstName = Boolean(credential.Alias?.FirstName?.trim());
|
||||
const hasLastName = Boolean(credential.Alias?.LastName?.trim());
|
||||
const hasNickName = Boolean(credential.Alias?.NickName?.trim());
|
||||
const hasBirthDate = isValidDate(credential.Alias?.BirthDate);
|
||||
|
||||
if (!hasFirstName && !hasLastName && !hasNickName && !hasBirthDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Alias</h2>
|
||||
{(hasFirstName || hasLastName) && (
|
||||
<FormInputCopyToClipboard
|
||||
id="fullName"
|
||||
label="Full Name"
|
||||
value={[credential.Alias?.FirstName, credential.Alias?.LastName].filter(Boolean).join(' ')}
|
||||
/>
|
||||
)}
|
||||
{hasFirstName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={credential.Alias?.FirstName}
|
||||
/>
|
||||
)}
|
||||
{hasLastName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={credential.Alias?.LastName}
|
||||
/>
|
||||
)}
|
||||
{hasBirthDate && (
|
||||
<FormInputCopyToClipboard
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
value={new Date(credential.Alias?.BirthDate).toISOString().split('T')[0]}
|
||||
/>
|
||||
)}
|
||||
{hasNickName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="nickName"
|
||||
label="Nickname"
|
||||
value={credential.Alias?.NickName ?? ''}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the notes block.
|
||||
*/
|
||||
const NotesBlock: React.FC<{ notes: string | undefined }> = ({ notes }) => {
|
||||
if (!notes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Notes</h2>
|
||||
<div className="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||
<p className="text-gray-900 dark:text-gray-100 whitespace-pre-wrap">
|
||||
{notes}
|
||||
</p>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Credential details page.
|
||||
*/
|
||||
const CredentialDetails: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = (): boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = (): void => {
|
||||
const width = 380;
|
||||
const height = 600;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/credentials/${id}`,
|
||||
'CredentialDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
window.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the email domain is supported.
|
||||
*/
|
||||
const isEmailDomainSupported = (email: string): boolean => {
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const publicDomains = dbContext.publicEmailDomains ?? [];
|
||||
const privateDomains = dbContext.privateEmailDomains ?? [];
|
||||
|
||||
return [...publicDomains, ...privateDomains].some(supportedDomain =>
|
||||
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a date is valid.
|
||||
*/
|
||||
const isValidDate = useCallback((date: string | null | undefined): boolean => {
|
||||
if (!date || date === '0001-01-01 00:00:00') {
|
||||
return false;
|
||||
}
|
||||
const dateObj = new Date(date);
|
||||
return !isNaN(dateObj.getTime());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopup()) {
|
||||
window.history.replaceState({}, '', `popup.html#/credentials`);
|
||||
window.history.pushState({}, '', `popup.html#/credentials/${id}`);
|
||||
}
|
||||
|
||||
if (!dbContext?.sqliteClient || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = dbContext.sqliteClient.getCredentialById(id);
|
||||
if (result) {
|
||||
setCredential(result);
|
||||
setIsInitialLoading(false);
|
||||
} else {
|
||||
console.error('Credential not found');
|
||||
navigate('/credentials');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading credential:', err);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading]);
|
||||
|
||||
if (!credential) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<HeaderBlock credential={credential} onOpenNewPopup={openInNewPopup} />
|
||||
|
||||
{credential.Alias?.Email && (
|
||||
<EmailBlock
|
||||
email={credential.Alias.Email}
|
||||
isSupported={isEmailDomainSupported(credential.Alias.Email)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TotpBlock credentialId={credential.Id} />
|
||||
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
|
||||
<AliasBlock
|
||||
credential={credential}
|
||||
isValidDate={isValidDate}
|
||||
/>
|
||||
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialDetails;
|
||||
@@ -0,0 +1,211 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { Credential } from '../../../utils/types/Credential';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import ReloadButton from '../components/ReloadButton';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
/**
|
||||
* Credentials list page.
|
||||
*/
|
||||
const CredentialsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Get the display text for a credential, showing username by default,
|
||||
* falling back to email only if username is null/undefined
|
||||
*/
|
||||
const getCredentialDisplayText = (cred: Credential): string => {
|
||||
const username = cred.Username ?? '';
|
||||
|
||||
// Show username if available.
|
||||
if (username.length > 0) {
|
||||
return username;
|
||||
}
|
||||
|
||||
// Show email if username is not available.
|
||||
const email = cred.Alias?.Email ?? '';
|
||||
if (email.length > 0) {
|
||||
return email;
|
||||
}
|
||||
|
||||
// Show empty string if neither username nor email is available.
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
|
||||
|
||||
/**
|
||||
* Retrieve latest vault and refresh the credentials list.
|
||||
*/
|
||||
const onRefresh = useCallback(async () : Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do status check first to ensure the extension is (still) supported.
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If the vault revision is the same or lower, (re)load existing credentials.
|
||||
if (statusResponse.vaultRevision <= dbContext.vaultRevision) {
|
||||
const results = dbContext.sqliteClient.getAllCredentials();
|
||||
setCredentials(results);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the vault revision is higher, fetch the latest vault and initialize the SQLite context again.
|
||||
* This will trigger a new credentials list refresh.
|
||||
*/
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
await webApi.logout(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
|
||||
// Initialize the SQLite context again with the newly retrieved decrypted blob
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
} catch (err) {
|
||||
console.error('Refresh error:', err);
|
||||
}
|
||||
}, [dbContext, webApi, hideLoading]);
|
||||
|
||||
/**
|
||||
* Manually refresh the credentials list.
|
||||
*/
|
||||
const onManualRefresh = async (): Promise<void> => {
|
||||
showLoading();
|
||||
await onRefresh();
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
/**
|
||||
* Load credentials list on mount and on sqlite client change.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Refresh credentials list when sqlite client is available.
|
||||
*/
|
||||
const refreshCredentials = async () : Promise<void> => {
|
||||
if (dbContext?.sqliteClient) {
|
||||
setIsLoading(true);
|
||||
await onRefresh();
|
||||
setIsLoading(false);
|
||||
|
||||
// Hide the global app initial loading state after the credentials list is loaded.
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
refreshCredentials();
|
||||
}, [dbContext?.sqliteClient, onRefresh, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchLower));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Credentials</h2>
|
||||
<ReloadButton onClick={onManualRefresh} />
|
||||
</div>
|
||||
|
||||
{credentials.length > 0 ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search credentials..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
autoFocus
|
||||
className="w-full p-2 mb-4 border dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{credentials.length === 0 ? (
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
|
||||
<p className="text-sm">
|
||||
Welcome to AliasVault!
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
If you want to create manual identities, open the full AliasVault app via the popout icon in the top right corner.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{filteredCredentials.map(cred => (
|
||||
<li key={cred.Id}>
|
||||
<button
|
||||
onClick={() => navigate(`/credentials/${cred.Id}`)}
|
||||
className="w-full p-2 border dark:border-gray-600 rounded flex items-center bg-white dark:bg-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<img
|
||||
src={SqliteClient.imgSrcFromBytes(cred.Logo)}
|
||||
alt={cred.ServiceName}
|
||||
className="w-8 h-8 mr-2 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/assets/images/service-placeholder.webp';
|
||||
}}
|
||||
/>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{cred.ServiceName}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{getCredentialDisplayText(cred)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialsList;
|
||||
281
browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Email } from '../../../utils/types/webapi/Email';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import { Attachment } from '../../../utils/types/webapi/Attachment';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import ConversionUtility from '../utils/ConversionUtility';
|
||||
|
||||
/**
|
||||
* Email details page.
|
||||
*/
|
||||
const EmailDetails: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<Email | null>(null);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Make sure the initial loading state is set to false when this component is loaded itself.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [setIsInitialLoading, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
if (isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `popup.html#/emails`);
|
||||
window.history.pushState({}, '', `popup.html#/emails/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the email.
|
||||
*/
|
||||
const loadEmail = async () : Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (!dbContext?.sqliteClient || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await webApi.get<Email>(`Email/${id}`);
|
||||
|
||||
// Decrypt email locally using public/private key pairs
|
||||
const encryptionKeys = dbContext.sqliteClient.getAllEncryptionKeys();
|
||||
const decryptedEmail = await EncryptionUtility.decryptEmail(response, encryptionKeys);
|
||||
setEmail(decryptedEmail);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadEmail();
|
||||
}, [id, dbContext?.sqliteClient, webApi, setIsLoading]);
|
||||
|
||||
/**
|
||||
* Handle deleting an email.
|
||||
*/
|
||||
const handleDelete = async () : Promise<void> => {
|
||||
try {
|
||||
await webApi.delete(`Email/${id}`);
|
||||
navigate('/emails');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete email');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/emails/${id}`,
|
||||
'EmailDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
// Close the current tab
|
||||
window.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle downloading an attachment.
|
||||
*/
|
||||
const handleDownloadAttachment = async (attachment: Attachment): Promise<void> => {
|
||||
try {
|
||||
// Get the encrypted attachment bytes from the API
|
||||
const base64EncryptedAttachment = await webApi.downloadBlobAndConvertToBase64(`Email/${id}/attachments/${attachment.id}`);
|
||||
|
||||
if (!dbContext?.sqliteClient || !email) {
|
||||
setError('Database context or email not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get encryption keys for decryption
|
||||
const encryptionKeys = dbContext.sqliteClient.getAllEncryptionKeys();
|
||||
|
||||
// Decrypt the attachment using ArrayBuffer
|
||||
const decryptedBytes = await EncryptionUtility.decryptAttachment(base64EncryptedAttachment, email, encryptionKeys);
|
||||
|
||||
if (!decryptedBytes) {
|
||||
setError('Failed to decrypt attachment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create blob from decrypted bytes with proper MIME type
|
||||
const blob = new Blob([decryptedBytes], { type: attachment.mimeType ?? 'application/octet-stream' });
|
||||
|
||||
// Create download link and trigger download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = attachment.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Cleanup
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (err) {
|
||||
console.error('handleDownloadAttachment error', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to download attachment');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return <div className="text-gray-500">Email not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={openInNewPopup}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
title="Open in new window"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="p-2 text-red-500 hover:text-red-600 rounded-md hover:bg-red-100 dark:hover:bg-red-900/20"
|
||||
title="Delete email"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>From: {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
<p>To: {email.toLocal}@{email.toDomain}</p>
|
||||
<p>Date: {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Body */}
|
||||
<div className="bg-white">
|
||||
{email.messageHtml ? (
|
||||
<iframe
|
||||
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
|
||||
className="w-full min-h-[500px] border-0"
|
||||
title="Email content"
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
{email.messagePlain}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{email.attachments && email.attachments.length > 0 && (
|
||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Attachments
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{email.attachments.map((attachment) => (
|
||||
<button
|
||||
key={attachment.id}
|
||||
onClick={() => handleDownloadAttachment(attachment)}
|
||||
className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 text-left"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{attachment.filename} ({Math.ceil(attachment.filesize / 1024)} KB)
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailDetails;
|
||||
159
browser-extension/src/entrypoints/popup/pages/EmailsList.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { MailboxBulkRequest, MailboxBulkResponse } from '../../../utils/types/webapi/MailboxBulk';
|
||||
import { MailboxEmail } from '../../../utils/types/webapi/MailboxEmail';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import ReloadButton from '../components/ReloadButton';
|
||||
import { Link } from 'react-router-dom';
|
||||
/**
|
||||
* Emails list page.
|
||||
*/
|
||||
const EmailsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [emails, setEmails] = useState<MailboxEmail[]>([]);
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
|
||||
|
||||
/**
|
||||
* Loads emails from the web API.
|
||||
*/
|
||||
const loadEmails = useCallback(async () : Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get unique email addresses from all credentials.
|
||||
const emailAddresses = dbContext.sqliteClient.getAllEmailAddresses();
|
||||
|
||||
try {
|
||||
// For now we only show the latest 50 emails. No pagination.
|
||||
const data = await webApi.post<MailboxBulkRequest, MailboxBulkResponse>('EmailBox/bulk', {
|
||||
addresses: emailAddresses,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
// Decrypt emails locally using private key associated with the email address.
|
||||
const encryptionKeys = dbContext.sqliteClient.getAllEncryptionKeys();
|
||||
|
||||
// Decrypt emails locally using public/private key pairs.
|
||||
const decryptedEmails = await EncryptionUtility.decryptEmailList(data.mails, encryptionKeys);
|
||||
|
||||
setEmails(decryptedEmails);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error('Failed to load emails');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
}, [loadEmails]);
|
||||
|
||||
/**
|
||||
* Formats the date display for emails
|
||||
*/
|
||||
const formatEmailDate = (dateSystem: string): string => {
|
||||
const now = new Date();
|
||||
const emailDate = new Date(dateSystem);
|
||||
const secondsAgo = Math.floor((now.getTime() - emailDate.getTime()) / 1000);
|
||||
|
||||
if (secondsAgo < 60) {
|
||||
return 'just now';
|
||||
} else if (secondsAgo < 3600) {
|
||||
// Less than 1 hour ago
|
||||
const minutes = Math.floor(secondsAgo / 60);
|
||||
return `${minutes} ${minutes === 1 ? 'min' : 'mins'} ago`;
|
||||
} else if (secondsAgo < 86400) {
|
||||
// Less than 24 hours ago
|
||||
const hours = Math.floor(secondsAgo / 3600);
|
||||
return `${hours} ${hours === 1 ? 'hr' : 'hrs'} ago`;
|
||||
} else if (secondsAgo < 172800) {
|
||||
// Less than 48 hours ago
|
||||
return 'yesterday';
|
||||
} else {
|
||||
// Older than 48 hours
|
||||
return emailDate.toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: '2-digit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (emails.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Emails</h2>
|
||||
<ReloadButton onClick={loadEmails} />
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2">
|
||||
<p className="text-sm">
|
||||
You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Emails</h2>
|
||||
<ReloadButton onClick={loadEmails} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{emails.map((email) => (
|
||||
<Link
|
||||
key={email.id}
|
||||
to={`/emails/${email.id}`}
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="text-sm text-gray-900 dark:text-white mb-1 font-bold">
|
||||
{email.subject}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatEmailDate(email.dateSystem)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
||||
{email.messagePreview}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailsList;
|
||||
61
browser-extension/src/entrypoints/popup/pages/Home.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import Unlock from './Unlock';
|
||||
import Login from './Login';
|
||||
import UnlockSuccess from './UnlockSuccess';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
/**
|
||||
* Home page that shows the correct page based on the user's authentication state.
|
||||
*/
|
||||
const Home: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
|
||||
|
||||
// Initialization state.
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
const isAuthenticated = authContext.isLoggedIn;
|
||||
const isDatabaseAvailable = dbContext.dbAvailable;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable || isInlineUnlockMode);
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if the user is coming from the unlock page with mode=inline_unlock.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isInlineUnlockMode = urlParams.get('mode') === 'inline_unlock';
|
||||
setIsInlineUnlockMode(isInlineUnlockMode);
|
||||
|
||||
// Redirect to credentials if fully initialized and doesn't need unlock.
|
||||
if (isFullyInitialized && !requireLoginOrUnlock) {
|
||||
navigate('/credentials', { replace: true });
|
||||
}
|
||||
}, [isFullyInitialized, requireLoginOrUnlock, isInlineUnlockMode, navigate]);
|
||||
|
||||
// Show loading state if not fully initialized or when about to redirect to credentials.
|
||||
if (!isFullyInitialized || (isFullyInitialized && !requireLoginOrUnlock)) {
|
||||
// Global loading spinner will be shown by the parent component.
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsInitialLoading(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (!isDatabaseAvailable) {
|
||||
return <Unlock />;
|
||||
}
|
||||
|
||||
if (isInlineUnlockMode) {
|
||||
return <UnlockSuccess onClose={() => setIsInlineUnlockMode(false)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
360
browser-extension/src/entrypoints/popup/pages/Login.tsx
Normal file
@@ -0,0 +1,360 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
import { Buffer } from 'buffer';
|
||||
import Button from '../components/Button';
|
||||
import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { LoginResponse } from '../../../utils/types/webapi/Login';
|
||||
import LoginServerInfo from '../components/LoginServerInfo';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { ApiAuthError } from '../../../utils/types/errors/ApiAuthError';
|
||||
|
||||
/**
|
||||
* Login page
|
||||
*/
|
||||
const Login: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const [credentials, setCredentials] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
|
||||
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
||||
const [twoFactorCode, setTwoFactorCode] = useState('');
|
||||
const [clientUrl, setClientUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load the client URL from the storage.
|
||||
*/
|
||||
const loadClientUrl = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
setClientUrl(clientUrl);
|
||||
};
|
||||
loadClientUrl();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
*/
|
||||
const handleSubmit = async (e: React.FormEvent) : Promise<void> => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
// Clear global message if set with every login attempt.
|
||||
authContext.clearGlobalMessage();
|
||||
|
||||
// Use the srpUtil instance instead of the imported singleton
|
||||
const loginResponse = await srpUtil.initiateLogin(credentials.username);
|
||||
|
||||
// 1. Derive key from password using Argon2id
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
credentials.password,
|
||||
loginResponse.salt,
|
||||
loginResponse.encryptionType,
|
||||
loginResponse.encryptionSettings
|
||||
);
|
||||
|
||||
// Convert uint8 array to uppercase hex string which is expected by the server.
|
||||
const passwordHashString = Buffer.from(passwordHash).toString('hex').toUpperCase();
|
||||
|
||||
// Get the derived key as base64 string required for decryption.
|
||||
const passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
|
||||
// 2. Validate login with SRP protocol
|
||||
const validationResponse = await srpUtil.validateLogin(
|
||||
credentials.username,
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse
|
||||
);
|
||||
|
||||
// 3. Handle 2FA if required
|
||||
if (validationResponse.requiresTwoFactor) {
|
||||
// Store login response as we need it for 2FA validation
|
||||
setLoginResponse(loginResponse);
|
||||
// Store password hash string as we need it for 2FA validation
|
||||
setPasswordHashString(passwordHashString);
|
||||
// Store password hash base64 as we need it for decryption
|
||||
setPasswordHashBase64(passwordHashBase64);
|
||||
setTwoFactorRequired(true);
|
||||
// Show app.
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error('Login failed -- no token returned');
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(credentials.username, validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle two factor submit.
|
||||
*/
|
||||
const handleTwoFactorSubmit = async (e: React.FormEvent) : Promise<void> => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
if (!passwordHashString || !passwordHashBase64 || !loginResponse) {
|
||||
throw new Error('Required login data not found');
|
||||
}
|
||||
|
||||
// Validate that 2FA code is a 6-digit number
|
||||
const code = twoFactorCode.trim();
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new ApiAuthError('Please enter a valid 6-digit authentication code.');
|
||||
}
|
||||
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
credentials.username,
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse,
|
||||
parseInt(twoFactorCode)
|
||||
);
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error('Login failed -- no token returned');
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(credentials.username, validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// Reset 2FA state and login response as it's no longer needed
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
setPasswordHashString(null);
|
||||
setPasswordHashBase64(null);
|
||||
setLoginResponse(null);
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
console.error('2FA error:', err);
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle change
|
||||
*/
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) : void => {
|
||||
const { name, value } = e.target;
|
||||
setCredentials(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
if (twoFactorRequired) {
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleTwoFactorSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 mb-4">
|
||||
Please enter the authentication code from your authenticator app.
|
||||
</p>
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="twoFactorCode">
|
||||
Authentication Code
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="twoFactorCode"
|
||||
type="text"
|
||||
value={twoFactorCode}
|
||||
onChange={(e) => setTwoFactorCode(e.target.value)}
|
||||
placeholder="Enter 6-digit code"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full space-y-2">
|
||||
<Button type="submit">
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Reset the form.
|
||||
setCredentials({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
setPasswordHashString(null);
|
||||
setPasswordHashBase64(null);
|
||||
setLoginResponse(null);
|
||||
setError(null);
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4 text-center">
|
||||
Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">Log in to AliasVault</h2>
|
||||
<LoginServerInfo />
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="username">
|
||||
Username or email
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="name / name@company.com"
|
||||
value={credentials.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Button type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
No account yet?{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500"
|
||||
>
|
||||
Create new vault
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||