Merge pull request #310 from lanedirt/306-improve-ux-for-login--create-account-flow
Improve ux for login and create account flow
@@ -1,5 +1,9 @@
|
||||
<a href="/" class="flex items-center justify-center mb-8 text-2xl font-semibold lg:mb-10 dark:text-white">
|
||||
<img src="horizontal-logo-cropped.png" alt="AliasVault" class="img-fluid" style="max-width: 330px;"/>
|
||||
<span class="ps-2 self-center hidden sm:flex text-lg font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
|
||||
<a href="/">
|
||||
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
|
||||
<span>AliasVault</span>
|
||||
<span class="ps-2 self-center hidden sm:flex text-lg font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Forgot your password?
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">If you have forgotten your password, contact the server admin and consult the AliasVault documentation on how to reset your password.</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">If you have forgotten your password, contact the server admin or consult the AliasVault documentation on how to reset your password.</p>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
@implements IDisposable
|
||||
|
||||
<header>
|
||||
<nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4 bg-primary-100">
|
||||
<nav class="fixed z-30 w-full border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4 bg-primary-100">
|
||||
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
|
||||
<div class="flex justify-start items-center">
|
||||
<a href="/" class="flex mr-14">
|
||||
<img src="/icon-trimmed.png" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<img src="/img/logo.svg" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
|
||||
<span class="ps-2 self-center hidden sm:flex text-sm font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
|
||||
</a>
|
||||
|
||||
@@ -687,6 +687,10 @@ video {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -719,6 +723,10 @@ video {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.ms-1 {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
@@ -747,14 +755,6 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.ms-1 {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -798,6 +798,14 @@ video {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.h-20 {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
@@ -826,10 +834,6 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -846,6 +850,14 @@ video {
|
||||
width: 66.666667%;
|
||||
}
|
||||
|
||||
.w-20 {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -882,10 +894,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
@@ -1038,12 +1046,6 @@ video {
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -1130,11 +1132,6 @@ video {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-amber-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 230 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
@@ -1160,26 +1157,21 @@ video {
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-sky-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(186 230 253 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-yellow-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
@@ -1310,11 +1302,6 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 249 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@@ -1325,21 +1312,16 @@ video {
|
||||
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1490,6 +1472,11 @@ video {
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
@@ -1547,11 +1534,6 @@ video {
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-amber-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(180 83 9 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity));
|
||||
@@ -1627,11 +1609,6 @@ video {
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -1855,10 +1832,6 @@ video {
|
||||
--tw-ring-color: rgb(252 165 165 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-offset-2:focus {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
@@ -1873,9 +1846,9 @@ video {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.dark\:border-amber-800:is(.dark *) {
|
||||
.dark\:border-blue-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(146 64 14 / var(--tw-border-opacity));
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-500:is(.dark *) {
|
||||
@@ -1893,14 +1866,9 @@ video {
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-sky-800:is(.dark *) {
|
||||
.dark\:border-green-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(7 89 133 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-blue-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-red-500:is(.dark *) {
|
||||
@@ -1908,16 +1876,21 @@ video {
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-green-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-yellow-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -1943,9 +1916,14 @@ video {
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-500:is(.dark *) {
|
||||
.dark\:bg-green-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
|
||||
background-color: rgb(22 101 52 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-600:is(.dark *) {
|
||||
@@ -1958,34 +1936,14 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-slate-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-800:is(.dark *) {
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 101 52 / var(--tw-bg-opacity));
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-800:is(.dark *) {
|
||||
@@ -1993,23 +1951,18 @@ video {
|
||||
background-color: rgb(133 77 14 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900:is(.dark *) {
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
.dark\:text-amber-300:is(.dark *) {
|
||||
.dark\:text-blue-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 211 77 / var(--tw-text-opacity));
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-blue-400:is(.dark *) {
|
||||
@@ -2042,9 +1995,9 @@ video {
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-400:is(.dark *) {
|
||||
.dark\:text-green-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(74 222 128 / var(--tw-text-opacity));
|
||||
color: rgb(134 239 172 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-primary-200:is(.dark *) {
|
||||
@@ -2072,11 +2025,6 @@ video {
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-sky-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -2092,16 +2040,6 @@ video {
|
||||
color: rgb(254 240 138 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-blue-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-green-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(134 239 172 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
@@ -2131,11 +2069,6 @@ video {
|
||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-primary-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-primary-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
8
src/AliasVault.Admin/wwwroot/img/logo.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 115 KiB |
@@ -298,7 +298,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
|
||||
// Check if the refresh token is valid.
|
||||
var deviceIdentifier = GenerateDeviceIdentifier(Request);
|
||||
var existingToken = context.AliasVaultUserRefreshTokens.FirstOrDefault(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier);
|
||||
var existingToken = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier);
|
||||
if (existingToken == null || existingToken.Value != model.RefreshToken)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.Logout, AuthFailureReason.InvalidRefreshToken);
|
||||
@@ -321,10 +321,11 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterRequest model)
|
||||
{
|
||||
// Validate username, disallow "admin" as a username.
|
||||
if (string.Equals(model.Username, "admin", StringComparison.OrdinalIgnoreCase))
|
||||
// Validate the username.
|
||||
var (isValid, errorMessage) = ValidateUsername(model.Username);
|
||||
if (!isValid)
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(["Username 'admin' is not allowed."], 400));
|
||||
return BadRequest(ServerValidationErrorResponse.Create([errorMessage], 400));
|
||||
}
|
||||
|
||||
var user = new AliasVaultUser
|
||||
@@ -395,6 +396,97 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
return Ok(new PasswordChangeInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate username endpoint used to check if a username is available.
|
||||
/// </summary>
|
||||
/// <param name="model">ValidateUsernameRequest model.</param>
|
||||
/// <returns>IActionResult.</returns>
|
||||
[HttpPost("validate-username")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ValidateUsername([FromBody] ValidateUsernameRequest model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.Username))
|
||||
{
|
||||
return BadRequest("Username is required.");
|
||||
}
|
||||
|
||||
var normalizedUsername = NormalizeUsername(model.Username);
|
||||
var existingUser = await userManager.FindByNameAsync(normalizedUsername);
|
||||
|
||||
if (existingUser != null)
|
||||
{
|
||||
return BadRequest("Username is already in use.");
|
||||
}
|
||||
|
||||
// Validate the username
|
||||
var (isValid, errorMessage) = ValidateUsername(normalizedUsername);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
return BadRequest(errorMessage);
|
||||
}
|
||||
|
||||
return Ok("Username is available.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a username by trimming and lowercasing it.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to normalize.</param>
|
||||
/// <returns>The normalized username.</returns>
|
||||
private static string NormalizeUsername(string username)
|
||||
{
|
||||
return username.ToLowerInvariant().Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if a given username meets the required criteria.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to validate.</param>
|
||||
/// <returns>A tuple containing a boolean indicating if the username is valid, and an error message if it's invalid.</returns>
|
||||
private static (bool IsValid, string ErrorMessage) ValidateUsername(string username)
|
||||
{
|
||||
const int minimumUsernameLength = 3;
|
||||
const string adminUsername = "admin";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return (false, "Username cannot be empty or whitespace.");
|
||||
}
|
||||
|
||||
if (username.Length < minimumUsernameLength)
|
||||
{
|
||||
return (false, $"Username must be at least {minimumUsernameLength} characters long.");
|
||||
}
|
||||
|
||||
if (string.Equals(username, adminUsername, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (false, "Username 'admin' is not allowed.");
|
||||
}
|
||||
|
||||
// Check if it's a valid email address
|
||||
if (username.Contains('@'))
|
||||
{
|
||||
try
|
||||
{
|
||||
var addr = new System.Net.Mail.MailAddress(username);
|
||||
return (addr.Address == username, string.Empty);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, $"'{username}' is not a valid email address.");
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not an email, check if it only contains letters and digits
|
||||
if (!username.All(char.IsLetterOrDigit))
|
||||
{
|
||||
return (false, $"Username '{username}' is invalid, can only contain letters or digits.");
|
||||
}
|
||||
|
||||
return (true, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a device identifier based on request headers. This is used to associate refresh tokens
|
||||
/// with a specific device for a specific user.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<a href="/" class="flex items-center justify-center mb-8 text-2xl font-semibold lg:mb-10 dark:text-white">
|
||||
<img src="horizontal-logo-cropped.png" alt="AliasVault" class="img-fluid" style="max-width: 330px;"/>
|
||||
<a href="/">
|
||||
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
|
||||
<span>AliasVault</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
3
src/AliasVault.Client/Auth/Layout/EmptyLayout.razor
Normal file
@@ -0,0 +1,3 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
@@ -1,7 +1,7 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using AliasVault.Client.Auth.Components
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-6 pt-8 mx-auto md:h-screen pt:mt-0 dark:bg-gray-900">
|
||||
<div class="flex flex-col items-center justify-center px-6 pt-8 pb-8 mx-auto md:h-screen pt:mt-0 dark:bg-gray-900">
|
||||
<Logo />
|
||||
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<GlobalNotificationDisplay />
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ public class LoginBase : OwningComponentBase
|
||||
[Inject]
|
||||
public required NavigationManager NavigationManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UserRegistrationService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public required UserRegistrationService UserRegistrationService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HttpClient.
|
||||
/// </summary>
|
||||
@@ -51,6 +57,12 @@ public class LoginBase : OwningComponentBase
|
||||
[Inject]
|
||||
public required IJSRuntime Js { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JsInteropService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public required JsInteropService JsInteropService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DbService.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
|
||||
@if (ShowTwoFactorAuthStep)
|
||||
@if (_showTwoFactorAuthStep)
|
||||
{
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
<ServerValidationErrors @ref="_serverValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">Your login is protected with an authenticator app. Enter your authenticator code below.</p>
|
||||
<div class="w-full">
|
||||
<EditForm Model="LoginModel2Fa" FormName="login-with-2fa" OnValidSubmit="Handle2Fa" method="post" class="space-y-6">
|
||||
<EditForm Model="_loginModel2Fa" FormName="login-with-2fa" OnValidSubmit="Handle2Fa" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
|
||||
<InputNumber @bind-Value="LoginModel2Fa.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => LoginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => _loginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<InputCheckbox @bind-Value="LoginModel2Fa.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
|
||||
<InputCheckbox @bind-Value="_loginModel2Fa.RememberMachine" id="remember-machine" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"/>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember-machine" class="font-medium text-gray-900 dark:text-white">Remember this machine</label>
|
||||
@@ -42,25 +42,25 @@
|
||||
<button @onclick="LoginWithRecoveryCode" class="text-primary-600 hover:underline dark:text-primary-500">log in with a recovery code</button>.
|
||||
</p>
|
||||
}
|
||||
else if (ShowLoginWithRecoveryCodeStep)
|
||||
else if (_showLoginWithRecoveryCodeStep)
|
||||
{
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Recovery code verification
|
||||
</h2>
|
||||
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
<ServerValidationErrors @ref="_serverValidationErrors" />
|
||||
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
You have requested to log in with a recovery code. A recovery code is a one-time code that can be used to log in to your account.
|
||||
Note that if you don't manually disable 2FA after login, you will be asked for an authenticator code again at the next login.
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<EditForm Model="LoginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
|
||||
<EditForm Model="_loginModelRecoveryCode" FormName="login-with-recovery-code" OnValidSubmit="HandleRecoveryCode" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Recovery Code</label>
|
||||
<InputText @bind-Value="LoginModelRecoveryCode.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => LoginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
<InputText @bind-Value="_loginModelRecoveryCode.RecoveryCode" id="recovery-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
|
||||
<ValidationMessage For="() => _loginModelRecoveryCode.RecoveryCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
|
||||
</div>
|
||||
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Log in</button>
|
||||
</EditForm>
|
||||
@@ -73,28 +73,28 @@ else if (ShowLoginWithRecoveryCodeStep)
|
||||
else
|
||||
{
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Sign in to AliasVault
|
||||
Log in to AliasVault
|
||||
</h2>
|
||||
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator"/>
|
||||
<ServerValidationErrors @ref="ServerValidationErrors"/>
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator"/>
|
||||
<ServerValidationErrors @ref="_serverValidationErrors"/>
|
||||
|
||||
<EditForm Model="LoginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<EditForm Model="_loginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username or email</label>
|
||||
<InputTextField id="email" @bind-Value="LoginModel.Username" placeholder="name / name@company.com"/>
|
||||
<ValidationMessage For="() => LoginModel.Username"/>
|
||||
<InputTextField id="email" @bind-Value="_loginModel.Username" placeholder="name / name@company.com"/>
|
||||
<ValidationMessage For="() => _loginModel.Username"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="LoginModel.Password" type="password" placeholder="••••••••"/>
|
||||
<ValidationMessage For="() => LoginModel.Password"/>
|
||||
<InputTextField id="password" @bind-Value="_loginModel.Password" type="password" placeholder="••••••••"/>
|
||||
<ValidationMessage For="() => _loginModel.Password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<InputCheckbox @bind-Value="LoginModel.RememberMe" id="remember" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
<InputCheckbox @bind-Value="_loginModel.RememberMe" id="remember" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
|
||||
@@ -104,23 +104,23 @@ else
|
||||
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Not registered? <a href="/user/register" class="text-primary-700 hover:underline dark:text-primary-500">Create account</a>
|
||||
No account yet? <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">Create new vault</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly LoginModel LoginModel = new();
|
||||
private readonly LoginModel2Fa LoginModel2Fa = new();
|
||||
private readonly LoginModelRecoveryCode LoginModelRecoveryCode = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
private bool ShowTwoFactorAuthStep;
|
||||
private bool ShowLoginWithRecoveryCodeStep;
|
||||
private readonly LoginModel _loginModel = new();
|
||||
private readonly LoginModel2Fa _loginModel2Fa = new();
|
||||
private readonly LoginModelRecoveryCode _loginModelRecoveryCode = new();
|
||||
private FullScreenLoadingIndicator _loadingIndicator = new();
|
||||
private ServerValidationErrors _serverValidationErrors = new();
|
||||
private bool _showTwoFactorAuthStep;
|
||||
private bool _showLoginWithRecoveryCodeStep;
|
||||
|
||||
private SrpEphemeral ClientEphemeral = new();
|
||||
private SrpSession ClientSession = new();
|
||||
private byte[] PasswordHash = [];
|
||||
private SrpEphemeral _clientEphemeral = new();
|
||||
private SrpSession _clientSession = new();
|
||||
private byte[] _passwordHash = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -133,17 +133,30 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void LoginWithAuthenticator()
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
ShowLoginWithRecoveryCodeStep = false;
|
||||
ShowTwoFactorAuthStep = true;
|
||||
if (firstRender)
|
||||
{
|
||||
await Task.Delay(300); // Give time for the DOM to update
|
||||
await JsInteropService.FocusElementById("email");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoginWithAuthenticator()
|
||||
{
|
||||
_showLoginWithRecoveryCodeStep = false;
|
||||
_showTwoFactorAuthStep = true;
|
||||
StateHasChanged();
|
||||
|
||||
await Task.Delay(100); // Give time for the DOM to update
|
||||
await JsInteropService.FocusElementById("two-factor-code");
|
||||
}
|
||||
|
||||
private void LoginWithRecoveryCode()
|
||||
{
|
||||
ShowLoginWithRecoveryCodeStep = true;
|
||||
ShowTwoFactorAuthStep = false;
|
||||
_showLoginWithRecoveryCodeStep = true;
|
||||
_showTwoFactorAuthStep = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -152,33 +165,33 @@ else
|
||||
/// </summary>
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var errors = await ProcessLoginAsync();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If in debug mode show the actual exception.
|
||||
ServerValidationErrors.AddError(ex.ToString());
|
||||
// If in debug mode show the actual exception.
|
||||
_serverValidationErrors.AddError(ex.ToString());
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
// If in release mode show a generic error.
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
_serverValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
}
|
||||
#endif
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
_loadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,12 +199,12 @@ else
|
||||
/// Process the login request using username and password.
|
||||
/// </summary>
|
||||
/// <returns>List of errors if something went wrong.</returns>
|
||||
protected async Task<List<string>> ProcessLoginAsync()
|
||||
private async Task<List<string>> ProcessLoginAsync()
|
||||
{
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Sanitize username
|
||||
var username = LoginModel.Username.ToLowerInvariant().Trim();
|
||||
var username = _loginModel.Username.ToLowerInvariant().Trim();
|
||||
|
||||
// Send request to server with username to get server ephemeral public key.
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginInitiateRequest(username));
|
||||
@@ -208,24 +221,24 @@ else
|
||||
return
|
||||
[
|
||||
"An error occurred while processing the login request.",
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Client derives shared session key.
|
||||
PasswordHash = await Encryption.DeriveKeyFromPasswordAsync(LoginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(PasswordHash).Replace("-", string.Empty);
|
||||
_passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty);
|
||||
|
||||
ClientEphemeral = Srp.GenerateEphemeralClient();
|
||||
_clientEphemeral = Srp.GenerateEphemeralClient();
|
||||
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString);
|
||||
ClientSession = Srp.DeriveSessionClient(
|
||||
_clientSession = Srp.DeriveSessionClient(
|
||||
privateKey,
|
||||
ClientEphemeral.Secret,
|
||||
_clientEphemeral.Secret,
|
||||
loginResponse.ServerEphemeral,
|
||||
loginResponse.Salt,
|
||||
username);
|
||||
|
||||
// 4. Client sends proof of session key to server.
|
||||
result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(username, LoginModel.RememberMe, ClientEphemeral.Public, ClientSession.Proof));
|
||||
result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));
|
||||
responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
@@ -239,14 +252,13 @@ else
|
||||
return
|
||||
[
|
||||
"An error occurred while processing the login request.",
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
// Check if 2FA is required, if yes, show 2FA step.
|
||||
if (validateLoginResponse.RequiresTwoFactor)
|
||||
{
|
||||
ShowTwoFactorAuthStep = true;
|
||||
StateHasChanged();
|
||||
await LoginWithAuthenticator();
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -259,23 +271,23 @@ else
|
||||
/// </summary>
|
||||
private async Task HandleRecoveryCode()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Sanitize username
|
||||
var username = LoginModel.Username.ToLowerInvariant().Trim();
|
||||
var username = _loginModel.Username.ToLowerInvariant().Trim();
|
||||
|
||||
// Validate 2-factor auth code auth and login
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, LoginModel.RememberMe, ClientEphemeral.Public, ClientSession.Proof, LoginModelRecoveryCode.RecoveryCode));
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModelRecoveryCode.RecoveryCode));
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -283,32 +295,32 @@ else
|
||||
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
|
||||
if (validateLoginResponse == null)
|
||||
{
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request.");
|
||||
_serverValidationErrors.AddError("An error occurred while processing the login request.");
|
||||
return;
|
||||
}
|
||||
|
||||
var errors = await ProcessLoginVerify(validateLoginResponse);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If in debug mode show the actual exception.
|
||||
ServerValidationErrors.AddError(ex.ToString());
|
||||
_serverValidationErrors.AddError(ex.ToString());
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
// If in release mode show a generic error.
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
_serverValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
}
|
||||
#endif
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
_loadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,23 +329,23 @@ else
|
||||
/// </summary>
|
||||
private async Task Handle2Fa()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Sanitize username
|
||||
var username = LoginModel.Username.ToLowerInvariant().Trim();
|
||||
var username = _loginModel.Username.ToLowerInvariant().Trim();
|
||||
|
||||
// Validate 2-factor auth code auth and login
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, LoginModel.RememberMe, ClientEphemeral.Public, ClientSession.Proof, LoginModel2Fa.TwoFactorCode ?? 0));
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModel2Fa.TwoFactorCode ?? 0));
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -341,32 +353,32 @@ else
|
||||
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
|
||||
if (validateLoginResponse == null)
|
||||
{
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request.");
|
||||
_serverValidationErrors.AddError("An error occurred while processing the login request.");
|
||||
return;
|
||||
}
|
||||
|
||||
var errors = await ProcessLoginVerify(validateLoginResponse);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
_serverValidationErrors.AddError(error);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If in debug mode show the actual exception.
|
||||
ServerValidationErrors.AddError(ex.ToString());
|
||||
_serverValidationErrors.AddError(ex.ToString());
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
// If in release mode show a generic error.
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
_serverValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
}
|
||||
#endif
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
_loadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,14 +388,14 @@ else
|
||||
private async Task<List<string>> ProcessLoginVerify(ValidateLoginResponse validateLoginResponse)
|
||||
{
|
||||
// 5. Client verifies proof.
|
||||
Srp.VerifySession(ClientEphemeral.Public, ClientSession, validateLoginResponse.ServerSessionProof);
|
||||
Srp.VerifySession(_clientEphemeral.Public, _clientSession, validateLoginResponse.ServerSessionProof);
|
||||
|
||||
// Store the tokens in local storage.
|
||||
await AuthService.StoreAccessTokenAsync(validateLoginResponse.Token!.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(validateLoginResponse.Token!.RefreshToken);
|
||||
|
||||
// Store the encryption key in memory.
|
||||
await AuthService.StoreEncryptionKeyAsync(PasswordHash);
|
||||
await AuthService.StoreEncryptionKeyAsync(_passwordHash);
|
||||
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
@@ -1,50 +1,42 @@
|
||||
@page "/user/register"
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@attribute [AllowAnonymous]
|
||||
@layout Auth.Layout.MainLayout
|
||||
@inject HttpClient Http
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@inject IConfiguration Configuration
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Create a new AliasVault account
|
||||
</h2>
|
||||
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
|
||||
<ServerValidationErrors @ref="_serverValidationErrors" />
|
||||
|
||||
<EditForm Model="RegisterModel" OnValidSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<EditForm Model="_registerModel" OnValidSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your username or email</label>
|
||||
<InputTextField id="email" @bind-Value="RegisterModel.Username" placeholder="name / name@company.com" />
|
||||
<ValidationMessage For="() => RegisterModel.Username"/>
|
||||
<InputTextField id="email" @bind-Value="_registerModel.Username" placeholder="name / name@company.com" />
|
||||
<ValidationMessage For="() => _registerModel.Username"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="RegisterModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => RegisterModel.Password"/>
|
||||
<InputTextField id="password" @bind-Value="_registerModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => _registerModel.Password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<InputTextField id="password2" @bind-Value="RegisterModel.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => RegisterModel.PasswordConfirm"/>
|
||||
<InputTextField id="password2" @bind-Value="_registerModel.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => _registerModel.PasswordConfirm"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<InputCheckbox id="terms" @bind-Value="RegisterModel.AcceptTerms" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
<InputCheckbox id="terms" @bind-Value="_registerModel.AcceptTerms" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="terms" class="font-medium text-gray-900 dark:text-white">I accept the <a href="https://github.com/lanedirt/AliasVault/blob/main/LICENSE.md" target="_blank" class="text-primary-700 hover:underline dark:text-primary-500">Terms and Conditions</a></label>
|
||||
<ValidationMessage For="() => RegisterModel.AcceptTerms"/>
|
||||
<ValidationMessage For="() => _registerModel.AcceptTerms"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,73 +47,27 @@
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private readonly RegisterModel RegisterModel = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
private readonly RegisterModel _registerModel = new();
|
||||
private FullScreenLoadingIndicator _loadingIndicator = new();
|
||||
private ServerValidationErrors _serverValidationErrors = new();
|
||||
|
||||
async Task HandleRegister()
|
||||
private async Task HandleRegister()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
_loadingIndicator.Show();
|
||||
_serverValidationErrors.Clear();
|
||||
|
||||
try
|
||||
var (success, errorMessage) = await UserRegistrationService.RegisterUserAsync(_registerModel.Username, _registerModel.Password);
|
||||
|
||||
if (success)
|
||||
{
|
||||
var client = new SrpClient();
|
||||
var salt = client.GenerateSalt();
|
||||
|
||||
byte[] passwordHash;
|
||||
string encryptionType = Defaults.EncryptionType;
|
||||
string encryptionSettings = Defaults.EncryptionSettings;
|
||||
if (Configuration["CryptographyOverrideType"] is not null && Configuration["CryptographyOverrideSettings"] is not null) {
|
||||
// If cryptography type and settings override are present in appsettings.json, use them instead of defaults
|
||||
// declared in code. This is used in certain cases e.g. E2E tests to speed up the process.
|
||||
encryptionType = Configuration["CryptographyOverrideType"]!;
|
||||
encryptionSettings = Configuration["CryptographyOverrideSettings"]!;
|
||||
}
|
||||
|
||||
passwordHash = await Encryption.DeriveKeyFromPasswordAsync(RegisterModel.Password, salt, encryptionType, encryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
var srpSignup = Srp.PasswordChangeAsync(client, salt, RegisterModel.Username, passwordHashString);
|
||||
|
||||
var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings);
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/register", registerRequest);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
{
|
||||
// Store the encryption key in memory.
|
||||
await AuthService.StoreEncryptionKeyAsync(passwordHash);
|
||||
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the case where the token is not present in the response
|
||||
ServerValidationErrors.AddError("An error occured during registration.");
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
_serverValidationErrors.AddError(errorMessage ?? "An error occurred during registration.");
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
_loadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.EmptyLayout
|
||||
@attribute [AllowAnonymous]
|
||||
@inject IConfiguration Configuration
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using SecureRemotePassword
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
<div class="relative inset-0 mt-10 z-10">
|
||||
<GlobalNotificationDisplay />
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The username to use for the new account.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The password to use for the new account.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
await CompleteSetup();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteSetup()
|
||||
{
|
||||
StateHasChanged();
|
||||
|
||||
var (success, errorMessage) = await UserRegistrationService.RegisterUserAsync(Username, Password);
|
||||
|
||||
if (success)
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
IsLoading = false;
|
||||
GlobalNotificationService.AddErrorMessage(errorMessage ?? "An error occurred during registration.", true);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@implements IDisposable
|
||||
@using System.Timers
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="absolute inset-0 flex justify-center items-center z-10">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="@(_isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6">
|
||||
<div class="flex items-start mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-10 w-10 rounded-full" src="/img/avatar.webp" alt="AliasVault Assistant">
|
||||
</div>
|
||||
<div class="ml-3 bg-blue-100 dark:bg-blue-900 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
Great! Now, let's set up your master password for AliasVault.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-6 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-900 dark:text-gray-100">
|
||||
<p class="text-sm font-semibold">
|
||||
Important: This master password will be used to encrypt your vault. It should be a long, complex string that you can remember. If you forget this password, your data will be permanently inaccessible.
|
||||
</p>
|
||||
<ul class="text-sm mt-3 list-disc list-inside">
|
||||
<li>Your master password never leaves your device</li>
|
||||
<li>The server has no access to your unencrypted data</li>
|
||||
<li>Even the server admin cannot restore your access if you forget this password</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="">
|
||||
<EditFormRow Id="password" Label="Master Password" @bind-Value="Password" Type="password" Placeholder="Enter your master password" OnFocus="@OnPasswordInputFocus"/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<EditFormRow Id="confirmPassword" Label="Confirm Master Password" @bind-Value="ConfirmPassword" Type="password" Placeholder="Confirm your master password" OnFocus="@OnPasswordInputFocus" />
|
||||
</div>
|
||||
@if (_isValidating)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Validating password...</div>
|
||||
}
|
||||
else if (_isValid)
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="mt-2 text-sm text-yellow-600 dark:text-yellow-400">@_errorMessage</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mt-2 text-sm text-green-600 dark:text-green-400">Password is valid and strong!</div>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="mt-2 text-sm text-red-600 dark:text-red-400">@_errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The event callback for when the password changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> OnPasswordChange { get; set; }
|
||||
|
||||
private string Password
|
||||
{
|
||||
get => _password;
|
||||
set
|
||||
{
|
||||
if (_password != value)
|
||||
{
|
||||
_password = value;
|
||||
ValidatePassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
set
|
||||
{
|
||||
if (_confirmPassword != value)
|
||||
{
|
||||
_confirmPassword = value;
|
||||
ValidatePassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _password = string.Empty;
|
||||
private string _confirmPassword = string.Empty;
|
||||
private bool _isValid = false;
|
||||
private bool _isValidating = false;
|
||||
private string _errorMessage = string.Empty;
|
||||
private Timer? _debounceTimer;
|
||||
|
||||
private bool _isLoading = true;
|
||||
private Timer? _loadingTimer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_loadingTimer?.Dispose();
|
||||
_debounceTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_loadingTimer = new Timer(300);
|
||||
_loadingTimer.Elapsed += (sender, e) => FinishLoading();
|
||||
_loadingTimer.AutoReset = false;
|
||||
_loadingTimer.Start();
|
||||
|
||||
_debounceTimer = new Timer(300);
|
||||
_debounceTimer.Elapsed += async (sender, e) => await ValidatePasswordDebounced();
|
||||
_debounceTimer.AutoReset = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
await Task.Delay(100); // Give time for the DOM to update
|
||||
await JsInteropService.FocusElementById("password");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes the loading animation.
|
||||
/// </summary>
|
||||
private void FinishLoading()
|
||||
{
|
||||
_isLoading = false;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the password immediately.
|
||||
/// </summary>
|
||||
private void ValidatePassword()
|
||||
{
|
||||
_isValidating = true;
|
||||
_isValid = false;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
_debounceTimer?.Stop();
|
||||
_debounceTimer?.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the password after input has stopped.
|
||||
/// </summary>
|
||||
private async Task ValidatePasswordDebounced()
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (Password.Length < 10)
|
||||
{
|
||||
_isValidating = false;
|
||||
_isValid = false;
|
||||
_errorMessage = "Master password must be at least 10 characters long.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ConfirmPassword))
|
||||
{
|
||||
_isValidating = false;
|
||||
_isValid = false;
|
||||
_errorMessage = "Confirm your password by entering it again.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Password != ConfirmPassword)
|
||||
{
|
||||
_isValidating = false;
|
||||
_isValid = false;
|
||||
_errorMessage = "Passwords do not match.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// If password is valid.
|
||||
_isValid = true;
|
||||
_errorMessage = string.Empty;
|
||||
|
||||
// Show warning for passwords between 10 and 13 characters.
|
||||
if (Password.Length < 14)
|
||||
{
|
||||
_errorMessage = "Password is valid, but could be stronger if made longer.";
|
||||
}
|
||||
|
||||
await OnPasswordChange.InvokeAsync(Password);
|
||||
|
||||
_isValidating = false;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the password input focus.
|
||||
/// </summary>
|
||||
private void OnPasswordInputFocus(FocusEventArgs args)
|
||||
{
|
||||
// Reset validation state when the input is focused.
|
||||
_isValid = false;
|
||||
_isValidating = false;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
@implements IDisposable
|
||||
@using System.Timers
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="absolute inset-0 flex justify-center items-center z-10">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="@(_isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Please read and agree to the following terms and conditions before proceeding.
|
||||
</p>
|
||||
<div class="bg-gray-100 dark:bg-gray-700 rounded-lg p-4 mb-8 h-80 overflow-y-auto">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Terms and Conditions</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
AliasVault is designed to enhance your online security and protect your privacy. With AliasVault, you can create unique identities and email aliases for your various online accounts, helping you maintain control over your personal information and reduce the risk of identity theft.
|
||||
<br><br>
|
||||
By using AliasVault, you agree to the following terms:
|
||||
<br><br>
|
||||
1. You will not use AliasVault for any illegal purposes, including but not limited to fraud, identity theft, or impersonating real individuals.
|
||||
<br><br>
|
||||
2. You are responsible for maintaining the confidentiality of your account and any aliases created through AliasVault.
|
||||
<br><br>
|
||||
3. AliasVault reserves the right to terminate your account if we suspect any misuse or violation of these terms.
|
||||
<br><br>
|
||||
4. You understand that while AliasVault enhances your privacy, no system is completely foolproof, and you use the service at your own risk.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="agreeTerms" @bind="AgreedToTerms" @bind:after="OnAgreedToTerms" class="mr-2">
|
||||
<label for="agreeTerms" class="text-sm font-bold text-gray-600 dark:text-gray-400">
|
||||
I have read and agree to the Terms and Conditions
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user has agreed to the terms and conditions.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool AgreedToTerms { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The event callback for when the user has agreed to the terms and conditions.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<bool> OnAgreedToTermsChanged { get; set; }
|
||||
|
||||
private bool _isLoading = true;
|
||||
private Timer? _loadingTimer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_loadingTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_loadingTimer = new Timer(300);
|
||||
_loadingTimer.Elapsed += (sender, e) => FinishLoading();
|
||||
_loadingTimer.AutoReset = false;
|
||||
_loadingTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes the loading animation.
|
||||
/// </summary>
|
||||
private void FinishLoading()
|
||||
{
|
||||
_isLoading = false;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the user agreeing to the terms and conditions.
|
||||
/// </summary>
|
||||
private async Task OnAgreedToTerms()
|
||||
{
|
||||
await OnAgreedToTermsChanged.InvokeAsync(AgreedToTerms);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@implements IDisposable
|
||||
@using System.Timers
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="absolute inset-0 flex justify-center items-center z-10">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="@(_isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6 mb-6">
|
||||
<div class="flex items-start mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-10 w-10 rounded-full" src="/img/avatar.webp" alt="AliasVault Assistant">
|
||||
</div>
|
||||
<div class="ml-3 bg-blue-100 dark:bg-blue-900 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
Great! Now, let's set up your username for AliasVault.
|
||||
</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white mt-3">
|
||||
Please enter a username you'd like to use. This can be your email address or any unique name you prefer.
|
||||
</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white mt-3 font-semibold">
|
||||
Remember: This is what you'll use to log in later, so make sure it's something you'll remember!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Username" Placeholder="Enter your desired username or email" OnFocus="@OnUsernameInputFocus" />
|
||||
@if (_isValidating)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Validating username...</div>
|
||||
}
|
||||
else if (_isValid)
|
||||
{
|
||||
<div class="mt-2 text-sm text-green-600 dark:text-green-400">Username is available!</div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="mt-2 text-sm text-red-600 dark:text-red-400">@_errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The username that is previously entered by the user. When a user navigates with back/continue
|
||||
/// and entered a username already, the existing username might be provided by the parent component.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DefaultUsername { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The event callback for when the username changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> OnUsernameChange { get; set; }
|
||||
|
||||
private string _username = string.Empty;
|
||||
private bool _isValid = false;
|
||||
private bool _isValidating = false;
|
||||
private string _errorMessage = string.Empty;
|
||||
private Timer? _debounceTimer;
|
||||
|
||||
/// <summary>
|
||||
/// The username that is entered by the user. This is the value that will be validated and sent to the parent component.
|
||||
/// </summary>
|
||||
private string Username
|
||||
{
|
||||
get => _username;
|
||||
set
|
||||
{
|
||||
if (_username != value)
|
||||
{
|
||||
_username = value;
|
||||
ValidateUsername();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isLoading = true;
|
||||
private Timer? _loadingTimer;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_loadingTimer?.Dispose();
|
||||
_debounceTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Set the default username if provided.
|
||||
_username = DefaultUsername;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DefaultUsername))
|
||||
{
|
||||
await ValidateUsernameDebounced();
|
||||
}
|
||||
|
||||
_loadingTimer = new Timer(300);
|
||||
_loadingTimer.Elapsed += (sender, e) => FinishLoading();
|
||||
_loadingTimer.AutoReset = false;
|
||||
_loadingTimer.Start();
|
||||
|
||||
_debounceTimer = new Timer(300);
|
||||
_debounceTimer.Elapsed += async (sender, e) => await ValidateUsernameDebounced();
|
||||
_debounceTimer.AutoReset = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
await Task.Delay(100); // Give time for the DOM to update
|
||||
await JsInteropService.FocusElementById("username");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes the loading animation.
|
||||
/// </summary>
|
||||
private void FinishLoading()
|
||||
{
|
||||
_isLoading = false;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the username immediately.
|
||||
/// </summary>
|
||||
private void ValidateUsername()
|
||||
{
|
||||
_isValidating = true;
|
||||
_isValid = false;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
_debounceTimer?.Stop();
|
||||
_debounceTimer?.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the username after input has stopped.
|
||||
/// </summary>
|
||||
private async Task ValidateUsernameDebounced()
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Username))
|
||||
{
|
||||
_isValidating = false;
|
||||
_isValid = false;
|
||||
_errorMessage = "Username is required.";
|
||||
await OnUsernameChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.PostAsJsonAsync("api/v1/Auth/validate-username", new { Username });
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
_isValid = true;
|
||||
_errorMessage = string.Empty;
|
||||
await OnUsernameChange.InvokeAsync(Username);
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
_errorMessage = error;
|
||||
_isValid = false;
|
||||
await OnUsernameChange.InvokeAsync(string.Empty);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_errorMessage = "An error occurred while validating the username.";
|
||||
_isValid = false;
|
||||
await OnUsernameChange.InvokeAsync(string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isValidating = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the username input focus.
|
||||
/// </summary>
|
||||
private void OnUsernameInputFocus(FocusEventArgs args)
|
||||
{
|
||||
// Reset validation state when the input is focused
|
||||
_isValid = false;
|
||||
_isValidating = false;
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<p class="mt-12 text-gray-600 dark:text-gray-400 mb-4 text-center">
|
||||
AliasVault is a secure app which help you create and manage your online identities and passwords.
|
||||
Let's get you set up with your new vault.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The event that is triggered when the user clicks the next button.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback OnNext { get; set; }
|
||||
}
|
||||
198
src/AliasVault.Client/Auth/Pages/Setup/Setup.razor
Normal file
@@ -0,0 +1,198 @@
|
||||
@page "/user/setup"
|
||||
@using AliasVault.Client.Auth.Pages.Setup.Components
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.EmptyLayout
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900 flex items-center justify-center">
|
||||
<div class="w-full mx-auto lg:max-w-xl lg:bg-white lg:dark:bg-gray-800 lg:shadow-xl lg:rounded-lg lg:overflow-hidden">
|
||||
<div class="flex flex-col min-h-screen lg:min-h-0">
|
||||
<div class="flex-grow p-8 pb-0">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<button @onclick="GoBack" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 @(_currentStep == SetupStep.Welcome ? "invisible" : "")">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-grow text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">@GetStepTitle(_currentStep)</h2>
|
||||
</div>
|
||||
<button @onclick="CancelSetup" class="text-gray-500 -mt-1 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@if (GetProgressPercentage() > 0)
|
||||
{
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4 dark:bg-gray-700 mt-4">
|
||||
<div class="bg-primary-600 h-2.5 rounded-full" style="width: @(GetProgressPercentage())%"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@switch (_currentStep)
|
||||
{
|
||||
case SetupStep.Welcome:
|
||||
<WelcomeStep />
|
||||
break;
|
||||
case SetupStep.TermsAndConditions:
|
||||
<TermsAndConditionsStep
|
||||
AgreedToTerms="@_setupData.AgreedToTerms"
|
||||
OnAgreedToTermsChanged="@HandleAgreedToTermsChanged" />
|
||||
break;
|
||||
case SetupStep.Username:
|
||||
<UsernameStep
|
||||
DefaultUsername="@_setupData.Username"
|
||||
OnUsernameChange="@((string username) => { _setupData.Username = username; StateHasChanged(); })" />
|
||||
break;
|
||||
case SetupStep.Password:
|
||||
<PasswordStep OnPasswordChange="@((string pwd) => { _setupData.Password = pwd; StateHasChanged(); })" />
|
||||
break;
|
||||
case SetupStep.Creating:
|
||||
<CreatingStep Username="@_setupData.Username" Password="@_setupData.Password" />
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
<div class="p-8 bg-gray-100 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 lg:bg-transparent lg:dark:bg-transparent lg:border-0">
|
||||
@if (_currentStep == SetupStep.Password && !string.IsNullOrWhiteSpace(_setupData.Password))
|
||||
{
|
||||
<button @onclick="GoNext"
|
||||
class="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out">
|
||||
Create Account
|
||||
</button>
|
||||
}
|
||||
else if (_currentStep != SetupStep.Creating)
|
||||
{
|
||||
<button @onclick="GoNext"
|
||||
class="w-full py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out @(IsNextEnabled ? "" : "opacity-50 cursor-not-allowed")"
|
||||
disabled="@(!IsNextEnabled)">
|
||||
Continue
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private SetupStep _currentStep = SetupStep.Welcome;
|
||||
private readonly SetupData _setupData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the "Continue" button is enabled based on the current step and setup data.
|
||||
/// </summary>
|
||||
private bool IsNextEnabled => _currentStep switch
|
||||
{
|
||||
SetupStep.Welcome => true,
|
||||
SetupStep.TermsAndConditions => _setupData.AgreedToTerms,
|
||||
SetupStep.Username => !string.IsNullOrWhiteSpace(_setupData.Username),
|
||||
SetupStep.Password => !string.IsNullOrWhiteSpace(_setupData.Password),
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the title for the setup step.
|
||||
/// </summary>
|
||||
/// <param name="step">The current setup step.</param>
|
||||
/// <returns>The title for the setup step.</returns>
|
||||
private static string GetStepTitle(SetupStep step)
|
||||
{
|
||||
return step switch
|
||||
{
|
||||
SetupStep.Welcome => "Welcome to AliasVault",
|
||||
SetupStep.TermsAndConditions => "Using AliasVault",
|
||||
SetupStep.Username => "Choose Username",
|
||||
SetupStep.Password => "Set Password",
|
||||
SetupStep.Creating => "Creating Vault",
|
||||
_ => "Setup"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the previous step in the setup process.
|
||||
/// </summary>
|
||||
private void GoBack()
|
||||
{
|
||||
switch (_currentStep)
|
||||
{
|
||||
case SetupStep.TermsAndConditions:
|
||||
_currentStep = SetupStep.Welcome;
|
||||
break;
|
||||
case SetupStep.Username:
|
||||
_currentStep = SetupStep.TermsAndConditions;
|
||||
break;
|
||||
case SetupStep.Password:
|
||||
_currentStep = SetupStep.Username;
|
||||
break;
|
||||
case SetupStep.Creating:
|
||||
_currentStep = SetupStep.Password;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the next step in the setup process.
|
||||
/// </summary>
|
||||
private void GoNext()
|
||||
{
|
||||
_currentStep = _currentStep switch
|
||||
{
|
||||
SetupStep.Welcome => SetupStep.TermsAndConditions,
|
||||
SetupStep.TermsAndConditions => SetupStep.Username,
|
||||
SetupStep.Username => SetupStep.Password,
|
||||
SetupStep.Password => SetupStep.Creating,
|
||||
_ => _currentStep
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the setup process and navigates to the start page.
|
||||
/// </summary>
|
||||
private void CancelSetup()
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the change of the terms and conditions agreement.
|
||||
/// </summary>
|
||||
/// <param name="agreed">True if the terms and conditions are agreed to, false otherwise.</param>
|
||||
private void HandleAgreedToTermsChanged(bool agreed)
|
||||
{
|
||||
_setupData.AgreedToTerms = agreed;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing the different steps in the setup process.
|
||||
/// </summary>
|
||||
private enum SetupStep
|
||||
{
|
||||
Welcome,
|
||||
TermsAndConditions,
|
||||
Username,
|
||||
Password,
|
||||
Creating
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data class for storing setup data.
|
||||
/// </summary>
|
||||
private sealed class SetupData
|
||||
{
|
||||
public bool AgreedToTerms { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the progress percentage based on the current step in the setup process.
|
||||
/// </summary>
|
||||
/// <returns>The progress percentage as an integer.</returns>
|
||||
private int GetProgressPercentage()
|
||||
{
|
||||
return (int)_currentStep * 100 / (Enum.GetValues(typeof(SetupStep)).Length - 1);
|
||||
}
|
||||
}
|
||||
50
src/AliasVault.Client/Auth/Pages/Start.razor
Normal file
@@ -0,0 +1,50 @@
|
||||
@page "/user/start"
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.EmptyLayout
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
<div class="flex lg:min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div class="w-full max-w-7xl mx-auto flex flex-col lg:flex-row">
|
||||
<div class="hidden lg:flex lg:w-1/2 items-center justify-center p-8">
|
||||
<div class="text-white text-4xl font-bold">
|
||||
<img src="img/logo.svg" alt="AliasVault" class="w-64 h-64" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-1/2 flex items-center justify-center px-8 py-12">
|
||||
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8">
|
||||
<Logo />
|
||||
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200 mb-6">
|
||||
Password & Identity manager
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8">
|
||||
Your Privacy. Protected.
|
||||
</p>
|
||||
<div class="space-y-4">
|
||||
<a href="/user/setup" class="block w-full py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out text-center">
|
||||
Create new vault
|
||||
</a>
|
||||
<a href="/user/login" class="block w-full py-3 px-4 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-800 dark:text-white font-semibold rounded-lg transition duration-300 ease-in-out text-center">
|
||||
Log in with existing account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer ShowBorder="false"></Footer>
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity?.IsAuthenticated == true) {
|
||||
// Already authenticated, redirect to home page.
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ else if (IsWebAuthnLoading) {
|
||||
else
|
||||
{
|
||||
<div class="flex space-x-4">
|
||||
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="Bonnie image">
|
||||
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="User image">
|
||||
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">@Username</h2>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="messages-container grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="messages-container grid px-4 pt-6 lg:gap-4">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "success")
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Notes</h3>
|
||||
@((MarkupString)ConvertUrlsToLinks(Notes.Replace(Environment.NewLine, "<br>")))
|
||||
<div class="dark:text-gray-300">
|
||||
@((MarkupString)ConvertUrlsToLinks(Notes).Replace(Environment.NewLine, "<br>"))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
@@ -15,6 +17,14 @@
|
||||
private static string ConvertUrlsToLinks(string text)
|
||||
{
|
||||
string urlPattern = @"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})";
|
||||
return Regex.Replace(text, urlPattern, match => $"<a href=\"{match.Value}\" target=\"_blank\" class=\"text-blue-500 hover:underline\">{match.Value}</a>", RegexOptions.None, TimeSpan.FromMilliseconds(100));
|
||||
return Regex.Replace(text, urlPattern, match =>
|
||||
{
|
||||
string url = match.Value;
|
||||
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
|
||||
{
|
||||
url = "http://" + url;
|
||||
}
|
||||
return $"<a href=\"{url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-blue-500 hover:underline\">{match.Value}</a>";
|
||||
}, RegexOptions.None, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="relative">
|
||||
@if (Type == "textarea")
|
||||
{
|
||||
<textarea id="@Id" style="height: 200px;" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged"></textarea>
|
||||
<textarea id="@Id" style="height: 200px;" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder"></textarea>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" id="@Id" @onfocus="OnFocusEvent" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
<input type="@Type" id="@Id" autocomplete="off" @onfocus="OnFocusEvent" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder">
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
[Parameter]
|
||||
public EventCallback<string?> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = string.Empty;
|
||||
|
||||
private async Task OnInputChanged(ChangeEventArgs e)
|
||||
{
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.NavigateTo("/user/login");
|
||||
Navigation.NavigateTo("/user/start");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@implements IDisposable
|
||||
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 md:mb-0">
|
||||
© 2024 AliasVault. All rights reserved.
|
||||
</p>
|
||||
<ul class="flex flex-wrap items-center justify-center">
|
||||
<li>
|
||||
<a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
<footer class="relative lg:fixed bottom-0 left-0 right-0 dark:bg-gray-900 @(ShowBorder ? "border-t border-gray-200 dark:border-gray-700" : "")">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-center">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 lg:mb-0">
|
||||
© 2024 AliasVault. All rights reserved.
|
||||
</p>
|
||||
<div class="hidden lg:block text-center text-gray-400 text-sm">@RandomQuote</div>
|
||||
<ul class="flex flex-wrap items-center justify-center">
|
||||
<li>
|
||||
<a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="text-center text-gray-400 text-sm pt-4 pb-2">@RandomQuote</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the footer should have a border.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowBorder { get; set; } = true;
|
||||
|
||||
private static readonly string[] Quotes =
|
||||
[
|
||||
"Tip: Use the g+c (go create) keyboard shortcut to quickly create a new alias.",
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
<ConfirmModal />
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<TopMenu />
|
||||
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
<div class="flex pt-16 pb-4 lg:pb-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative z-10 w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
<main>
|
||||
<GlobalNotificationDisplay />
|
||||
@Body
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<main>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
|
||||
<div class="flex justify-start items-center">
|
||||
<a href="/" class="flex mr-0 sm:mr-4 lg:mr-8">
|
||||
<img src="/icon-trimmed.png" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<img src="/img/icon-nopadding.png" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ builder.Services.AddScoped(sp =>
|
||||
});
|
||||
builder.Services.AddTransient<AliasVaultApiHandlerService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<UserRegistrationService>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
||||
builder.Services.AddScoped<CredentialService>();
|
||||
builder.Services.AddScoped<DbService>();
|
||||
|
||||
101
src/AliasVault.Client/Services/Auth/UserRegistrationService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UserRegistrationService.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Services.Auth;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using AliasVault.Client.Utilities;
|
||||
using AliasVault.Cryptography.Client;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using SecureRemotePassword;
|
||||
|
||||
/// <summary>
|
||||
/// This service is responsible for registering a new user.
|
||||
/// </summary>
|
||||
public class UserRegistrationService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly AuthenticationStateProvider _authStateProvider;
|
||||
private readonly AuthService _authService;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserRegistrationService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">The HTTP client.</param>
|
||||
/// <param name="authStateProvider">The authentication state provider.</param>
|
||||
/// <param name="authService">The authentication service.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
public UserRegistrationService(
|
||||
HttpClient httpClient,
|
||||
AuthenticationStateProvider authStateProvider,
|
||||
AuthService authService,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_authStateProvider = authStateProvider;
|
||||
_authService = authService;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>A tuple indicating the success status and any error message.</returns>
|
||||
public async Task<(bool Success, string? ErrorMessage)> RegisterUserAsync(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new SrpClient();
|
||||
var salt = client.GenerateSalt();
|
||||
|
||||
string encryptionType = Defaults.EncryptionType;
|
||||
string encryptionSettings = Defaults.EncryptionSettings;
|
||||
if (_configuration["CryptographyOverrideType"] is not null && _configuration["CryptographyOverrideSettings"] is not null)
|
||||
{
|
||||
encryptionType = _configuration["CryptographyOverrideType"]!;
|
||||
encryptionSettings = _configuration["CryptographyOverrideSettings"]!;
|
||||
}
|
||||
|
||||
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(password, salt, encryptionType, encryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
var srpSignup = Srp.PasswordChangeAsync(client, salt, username, passwordHashString);
|
||||
|
||||
var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings);
|
||||
var result = await _httpClient.PostAsJsonAsync("api/v1/Auth/register", registerRequest);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
var errors = ApiResponseUtility.ParseErrorResponse(responseContent);
|
||||
return (false, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject == null)
|
||||
{
|
||||
return (false, "An error occurred during registration.");
|
||||
}
|
||||
|
||||
await _authService.StoreEncryptionKeyAsync(passwordHash);
|
||||
await _authService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await _authService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await _authStateProvider.GetAuthenticationStateAsync();
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"An error occurred: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,6 +554,40 @@ video {
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -570,6 +604,10 @@ video {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
@@ -600,6 +638,10 @@ video {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.bottom-1 {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
@@ -608,6 +650,10 @@ video {
|
||||
inset-inline-end: 0.25rem;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0px;
|
||||
}
|
||||
@@ -620,6 +666,10 @@ video {
|
||||
right: 2.5rem;
|
||||
}
|
||||
|
||||
.right-4 {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.top-12 {
|
||||
top: 3rem;
|
||||
}
|
||||
@@ -628,6 +678,18 @@ video {
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
.top-4 {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.left-4 {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -652,10 +714,6 @@ video {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.m-0 {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.\!m-0 {
|
||||
margin: 0px !important;
|
||||
}
|
||||
@@ -703,6 +761,10 @@ video {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -771,8 +833,32 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
.mt-24 {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
.-mt-2 {
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.-mt-1 {
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
@@ -822,6 +908,14 @@ video {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.h-20 {
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
@@ -838,6 +932,10 @@ video {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.h-64 {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem;
|
||||
}
|
||||
@@ -850,6 +948,14 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-40 {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.h-80 {
|
||||
height: 20rem;
|
||||
}
|
||||
|
||||
.h-2 {
|
||||
height: 0.5rem;
|
||||
}
|
||||
@@ -858,6 +964,10 @@ video {
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -870,6 +980,14 @@ video {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.w-20 {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.w-28 {
|
||||
width: 7rem;
|
||||
}
|
||||
@@ -914,6 +1032,10 @@ video {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.max-w-7xl {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
@@ -1186,6 +1308,10 @@ video {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-b-2 {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-l-0 {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
@@ -1198,9 +1324,13 @@ video {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-amber-200 {
|
||||
.border-t-2 {
|
||||
border-top-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 230 138 / var(--tw-border-opacity));
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-blue-700 {
|
||||
@@ -1243,14 +1373,19 @@ video {
|
||||
border-color: rgb(214 131 56 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-sky-200 {
|
||||
.border-yellow-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(186 230 253 / var(--tw-border-opacity));
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-50 {
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 251 235 / var(--tw-bg-opacity));
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
@@ -1303,6 +1438,11 @@ video {
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
|
||||
@@ -1373,31 +1513,21 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-sky-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 249 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 246 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1424,6 +1554,10 @@ video {
|
||||
fill: #d68338;
|
||||
}
|
||||
|
||||
.\!p-0 {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -1432,6 +1566,10 @@ video {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -1448,14 +1586,6 @@ video {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.\!p-0 {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1486,11 +1616,21 @@ video {
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.px-8 {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.py-12 {
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
@@ -1511,13 +1651,12 @@ video {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.py-6 {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
.pb-4 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
.pb-8 {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pr-10 {
|
||||
@@ -1540,6 +1679,10 @@ video {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-20 {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@@ -1552,6 +1695,26 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pt-24 {
|
||||
padding-top: 6rem;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.pt-12 {
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
.pt-14 {
|
||||
padding-top: 3.5rem;
|
||||
}
|
||||
|
||||
.pb-0 {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1582,6 +1745,16 @@ video {
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-5xl {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
@@ -1647,11 +1820,6 @@ video {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.text-amber-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(180 83 9 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
@@ -1667,6 +1835,11 @@ video {
|
||||
color: rgb(29 78 216 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
@@ -1747,19 +1920,14 @@ video {
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-sky-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(3 105 161 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-800 {
|
||||
.text-yellow-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
color: rgb(202 138 4 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-yellow-800 {
|
||||
@@ -1767,6 +1935,23 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-yellow-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(161 98 7 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.opacity-10 {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.opacity-100 {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.opacity-25 {
|
||||
opacity: 0.25;
|
||||
}
|
||||
@@ -1827,6 +2012,12 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.duration-150 {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
@@ -1964,6 +2155,11 @@ video {
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
@@ -1989,6 +2185,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(31 41 55 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -2099,16 +2300,16 @@ video {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.dark\:border-amber-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(146 64 14 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-blue-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-400:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-600:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
@@ -2119,16 +2320,6 @@ video {
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-sky-800:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(7 89 133 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-400:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-green-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
@@ -2144,6 +2335,21 @@ video {
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -2169,6 +2375,11 @@ video {
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 101 52 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
|
||||
@@ -2184,19 +2395,9 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-slate-800:is(.dark *) {
|
||||
.dark\:bg-red-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-400:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-white:is(.dark *) {
|
||||
@@ -2204,26 +2405,16 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-green-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 101 52 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(133 77 14 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -2238,11 +2429,6 @@ video {
|
||||
--tw-gradient-to: #f49541 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.dark\:text-amber-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(252 211 77 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-blue-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity));
|
||||
@@ -2253,6 +2439,16 @@ video {
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-100:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
@@ -2288,29 +2484,19 @@ video {
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-sky-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(125 211 252 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-100:is(.dark *) {
|
||||
.dark\:text-yellow-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(243 244 246 / var(--tw-text-opacity));
|
||||
color: rgb(250 204 21 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-gray-200:is(.dark *) {
|
||||
.dark\:text-yellow-200:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
color: rgb(254 240 138 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
@@ -2347,6 +2533,11 @@ video {
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-800:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-green-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||
@@ -2382,6 +2573,11 @@ video {
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-gray-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-500:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
@@ -2483,10 +2679,6 @@ video {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sm\:max-w-lg {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
@@ -2541,26 +2733,14 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md\:ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.md\:mr-6 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.md\:inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.md\:flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.md\:h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -2569,10 +2749,6 @@ video {
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.md\:max-w-xl {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2585,27 +2761,34 @@ video {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md\:justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.md\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.md\:py-10 {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.lg\:relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lg\:-left-16 {
|
||||
left: -4rem;
|
||||
}
|
||||
|
||||
.lg\:left-auto {
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.lg\:top-0 {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.lg\:order-1 {
|
||||
order: 1;
|
||||
}
|
||||
@@ -2618,8 +2801,8 @@ video {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.lg\:mb-10 {
|
||||
margin-bottom: 2.5rem;
|
||||
.lg\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.lg\:mr-8 {
|
||||
@@ -2634,6 +2817,10 @@ video {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lg\:block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lg\:flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -2642,17 +2829,99 @@ video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lg\:min-h-0 {
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
.lg\:min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.lg\:w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.lg\:w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.lg\:max-w-xl {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.lg\:flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lg\:flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.lg\:justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.lg\:gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lg\:overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lg\:rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.lg\:border-0 {
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
.lg\:bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.lg\:bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.lg\:pb-16 {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.lg\:pt-10 {
|
||||
padding-top: 2.5rem;
|
||||
}
|
||||
|
||||
.lg\:pt-8 {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.lg\:shadow-none {
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.lg\:shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.lg\:dark\:bg-gray-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.lg\:dark\:bg-transparent:is(.dark *) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@@ -2711,9 +2980,4 @@ video {
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.\32xl\:px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
8
src/AliasVault.Client/wwwroot/img/logo.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,19 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ValidateUsernameRequest.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// A request to validate a username.
|
||||
/// </summary>
|
||||
public class ValidateUsernameRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the username to validate.
|
||||
/// </summary>
|
||||
public required string Username { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -132,9 +132,9 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
Page = await Context.NewPageAsync();
|
||||
InputHelper = new(Page);
|
||||
|
||||
// Check that we get redirected to /user/login when accessing the root URL and not authenticated.
|
||||
// Check that we get redirected to /user/start when accessing the root URL and not authenticated.
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/login");
|
||||
await WaitForUrlAsync("user/start");
|
||||
|
||||
// Register a new account here because every test requires this.
|
||||
await Register();
|
||||
@@ -283,8 +283,10 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
protected async Task Login(bool rememberMe = false)
|
||||
{
|
||||
// Check that we are on the login page after navigating to the base URL.
|
||||
// We are expecting to not be authenticated and thus to be redirected to the login page.
|
||||
// We are expecting to not be authenticated and thus to be redirected to the start page.
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Log in with");
|
||||
await NavigateUsingBlazorRouter("user/login");
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
|
||||
// Try to log in with test credentials.
|
||||
@@ -315,7 +317,19 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
protected async Task Logout()
|
||||
{
|
||||
await NavigateUsingBlazorRouter("user/logout");
|
||||
await WaitForUrlAsync("user/login**", "Sign in to");
|
||||
await WaitForUrlAsync("user/start**", "Log in with");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hard refresh and navigate to the login page.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
protected async Task NavigateToLogin()
|
||||
{
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Log in with");
|
||||
await NavigateUsingBlazorRouter("user/login");
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -344,8 +358,7 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
protected async Task Register(bool checkForSuccess = true, string? username = null, string? password = null)
|
||||
{
|
||||
// Try to register a new account.
|
||||
var registerButton = await WaitForAndGetElement("a[href='/user/register']");
|
||||
await registerButton.ClickAsync();
|
||||
await NavigateUsingBlazorRouter("user/register");
|
||||
await WaitForUrlAsync("user/register");
|
||||
|
||||
// Try to register an account with the generated test credentials.
|
||||
|
||||
@@ -179,6 +179,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
|
||||
// Assert that the claim was created on the server.
|
||||
var claim = await ApiDbContext.UserEmailClaims.FirstOrDefaultAsync(x => x.Address == email);
|
||||
Assert.That(claim, Is.Not.Null, "Claim for email address not found in database. Check if credential creation and claim creation are working correctly.");
|
||||
|
||||
// Login as new user.
|
||||
await LogoutAndLoginAsNewUser();
|
||||
|
||||
@@ -27,8 +27,10 @@ public class CodeLockoutTests : TwoFactorAuthBase
|
||||
await DisableTwoFactorIfEnabled();
|
||||
await EnableTwoFactor();
|
||||
await Logout();
|
||||
await NavigateToLogin();
|
||||
|
||||
// Attempt to log in again with test credentials.
|
||||
await NavigateUsingBlazorRouter("user/login");
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
|
||||
// Wait for the page to fully load.
|
||||
|
||||
@@ -28,9 +28,7 @@ public class RecoveryLockoutTests : TwoFactorAuthBase
|
||||
await EnableTwoFactor();
|
||||
|
||||
await Logout();
|
||||
|
||||
// Attempt to log in again with test credentials.
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
await NavigateToLogin();
|
||||
|
||||
// Wait for the page to fully load.
|
||||
await Task.Delay(100);
|
||||
|
||||
@@ -38,9 +38,7 @@ public class TwoFactorAuthTests : TwoFactorAuthBase
|
||||
Assert.That(message, Does.Contain("Two-factor authentication is now successfully enabled."), "No success message displayed.");
|
||||
|
||||
await Logout();
|
||||
|
||||
// Attempt to log in again with test credentials.
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
await NavigateToLogin();
|
||||
|
||||
// Wait for the page to fully load.
|
||||
await Task.Delay(100);
|
||||
@@ -80,9 +78,7 @@ public class TwoFactorAuthTests : TwoFactorAuthBase
|
||||
var (_, recoveryCode) = await EnableTwoFactor();
|
||||
|
||||
await Logout();
|
||||
|
||||
// Attempt to log in again with test credentials.
|
||||
await WaitForUrlAsync("user/login", "Your username");
|
||||
await NavigateToLogin();
|
||||
|
||||
// Wait for the page to fully load.
|
||||
await Task.Delay(100);
|
||||
|
||||
@@ -15,7 +15,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[Category("ClientTests")]
|
||||
|
||||
[TestFixture]
|
||||
public class AuthTests : ClientPlaywrightTest
|
||||
{
|
||||
@@ -121,6 +120,7 @@ public class AuthTests : ClientPlaywrightTest
|
||||
public async Task PasswordAuthLockoutTest()
|
||||
{
|
||||
await Logout();
|
||||
await NavigateToLogin();
|
||||
|
||||
// Fill in wrong password 11 times. After 11 times, the account should be locked.
|
||||
// Note: the actual lockout happens on the 10th wrong attempt, but the lockout message is only displayed
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UserSetupTests.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.E2ETests.Tests.Client.Shard4;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for the registration process via the wizard / tutorial interface (/user/setup).
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[Category("ClientTests")]
|
||||
[TestFixture]
|
||||
public class UserSetupTests : ClientPlaywrightTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test if registering an account through the tutorial interface works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(1)]
|
||||
public async Task UserSetupProcessTest()
|
||||
{
|
||||
// Logout.
|
||||
await Logout();
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Create new vault");
|
||||
|
||||
// Click the "Create new vault" anchor tag.
|
||||
var createVaultButton = await WaitForAndGetElement("a:has-text('Create new vault')");
|
||||
await createVaultButton.ClickAsync();
|
||||
|
||||
// Wait for the setup page to load.
|
||||
await WaitForUrlAsync("user/setup", "Welcome to AliasVault");
|
||||
|
||||
// Press the continue button.
|
||||
var continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the terms and conditions to load.
|
||||
await WaitForUrlAsync("user/setup", "Terms and Conditions");
|
||||
|
||||
// Accept the terms and conditions.
|
||||
var acceptTermsCheckbox = await WaitForAndGetElement("input[id='agreeTerms']");
|
||||
await acceptTermsCheckbox.CheckAsync();
|
||||
|
||||
// Wait for the continue button to be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the continue button.
|
||||
continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the username step to load.
|
||||
await WaitForUrlAsync("user/setup", "Username");
|
||||
var usernameField = await WaitForAndGetElement("input[id='username']");
|
||||
await usernameField.FillAsync(TestUserUsername + "1"); // Add a suffix to the username to make it unique.
|
||||
|
||||
// Wait for the continue button to be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the continue button.
|
||||
continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the password step to load.
|
||||
await WaitForUrlAsync("user/setup", "Set Password");
|
||||
var passwordField = await WaitForAndGetElement("input[id='password']");
|
||||
await passwordField.FillAsync(TestUserPassword);
|
||||
var confirmPasswordField = await WaitForAndGetElement("input[id='confirmPassword']");
|
||||
await confirmPasswordField.FillAsync(TestUserPassword);
|
||||
|
||||
// Wait for the create account button to show up and be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the create account button.
|
||||
continueButton = await WaitForAndGetElement("button:has-text('Create Account')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Verify that we end up on the welcome page which confirms the account has been successfully created.
|
||||
await WaitForUrlAsync("welcome**", "Getting Started");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the "Username is already in use" error appears when trying to register with an existing username.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(2)]
|
||||
public async Task UserSetupUsernameExistsTest()
|
||||
{
|
||||
// Logout.
|
||||
await Logout();
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Create new vault");
|
||||
|
||||
// Click the "Create new vault" anchor tag.
|
||||
var createVaultButton = await WaitForAndGetElement("a:has-text('Create new vault')");
|
||||
await createVaultButton.ClickAsync();
|
||||
|
||||
// Wait for the setup page to load.
|
||||
await WaitForUrlAsync("user/setup", "Welcome to AliasVault");
|
||||
|
||||
// Press the continue button.
|
||||
var continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the terms and conditions to load.
|
||||
await WaitForUrlAsync("user/setup", "Terms and Conditions");
|
||||
|
||||
// Accept the terms and conditions.
|
||||
var acceptTermsCheckbox = await WaitForAndGetElement("input[id='agreeTerms']");
|
||||
await acceptTermsCheckbox.CheckAsync();
|
||||
|
||||
// Wait for the continue button to be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the continue button.
|
||||
continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the username step to load.
|
||||
await WaitForUrlAsync("user/setup", "Username");
|
||||
var usernameField = await WaitForAndGetElement("input[id='username']");
|
||||
await usernameField.FillAsync(TestUserUsername); // Use the existing username without appending "1"
|
||||
|
||||
// Check if the "Username is already in use" error message appears
|
||||
var errorMessage = await WaitForAndGetElement("text='Username is already in use.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username is already in use' error message should appear.");
|
||||
}
|
||||
}
|
||||
8
wwwroot/js/transitionEffect.js
Normal file
@@ -0,0 +1,8 @@
|
||||
function startTransitionEffect() {
|
||||
const overlay = document.getElementById('transitionOverlay');
|
||||
overlay.style.transform = 'scale(1)';
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/user/register';
|
||||
}, 500);
|
||||
}
|
||||