diff --git a/.gitignore b/.gitignore index cd38e2e7b..1eb6d99f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ target logs attachments *.patch +.env +*.log + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..15d7a08ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + postgres-jira-prod: + image: postgres:16 + container_name: postgres-jira-prod + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_DB: jira + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "5434:5432" + volumes: + - pgprod_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d jira"] + interval: 5s + timeout: 3s + retries: 10 + + postgres-jira-test: + image: postgres:16 + container_name: postgres-jira-test + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_DB: jira-test + POSTGRES_USER: ${TEST_DB_USER} + POSTGRES_PASSWORD: ${TEST_DB_PASSWORD} + ports: + - "5433:5432" + volumes: + - pgtest_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${TEST_DB_USER} -d jira-test"] + interval: 5s + timeout: 3s + retries: 10 + +volumes: + pgprod_data: + pgtest_data: diff --git a/pom.xml b/pom.xml index f6c152c68..d104abedb 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,8 @@ 17 + 17 + 17 2.0.2 1.5.3.Final UTF-8 @@ -85,6 +87,7 @@ org.postgresql postgresql + 42.7.3 runtime @@ -142,6 +145,14 @@ junit-platform-launcher test + + + + com.h2database + h2 + test + + @@ -170,7 +181,12 @@ org.apache.maven.plugins maven-surefire-plugin + 3.2.5 + true + + test + -Dfile.encoding=UTF-8 diff --git a/project-feature.zip b/project-feature.zip new file mode 100644 index 000000000..540751c8d Binary files /dev/null and b/project-feature.zip differ diff --git a/project-feature1.zip b/project-feature1.zip new file mode 100644 index 000000000..5c885ccc2 Binary files /dev/null and b/project-feature1.zip differ diff --git a/resources/mails/email-confirmation.html b/resources/mails/email-confirmation.html index 106e6129a..408eb641b 100644 --- a/resources/mails/email-confirmation.html +++ b/resources/mails/email-confirmation.html @@ -1,13 +1,20 @@ - + - JiraRush - подтверждение почты - + JiraRush - подтверждение почты + -

-

Чтобы завершить настройку учетной записи и начать пользоваться JiraRush, подтвердите, что вы правильно указали вашу - электронную почту.

-Подтвердить почту +

Привет, Ім’я.

+

+ Чтобы завершить настройку учетной записи и начать пользоваться JiraRush, подтвердите, что вы правильно указали вашу электронную почту. +

+

+ Подтвердить почту +

+

+ Если вы не регистрировались в JiraRush, просто проигнорируйте это письмо. +

+

С уважением, команда JiraRush

- \ No newline at end of file + diff --git a/resources/mails/password-reset.html b/resources/mails/password-reset.html index b37a49007..607ccc05c 100644 --- a/resources/mails/password-reset.html +++ b/resources/mails/password-reset.html @@ -1,12 +1,20 @@ - + - JiraRush - установить новый пароль - + JiraRush — встановити новий пароль + -

-

-Установить пароль +

Привіт, Ім’я.

+

+ Ми отримали запит на встановлення нового пароля JiraRush для облікового запису: email. +

+

+ Встановити пароль +

+

+ Якщо ви не надсилали цей запит, просто проігноруйте цей лист. +

+

З повагою, команда JiraRush

- \ No newline at end of file + diff --git a/resources/static/fontawesome/css/all.css b/resources/static/fontawesome/css/all.css index af5980828..6a16cc2f0 100644 --- a/resources/static/fontawesome/css/all.css +++ b/resources/static/fontawesome/css/all.css @@ -8603,10 +8603,6 @@ readers do not read off random characters that represent icons */ content: "\f3e8"; } -.fa-vk:before { - content: "\f189"; -} - .fa-untappd:before { content: "\f405"; } @@ -9955,10 +9951,6 @@ readers do not read off random characters that represent icons */ content: "\f3bc"; } -.fa-yandex:before { - content: "\f413"; -} - .fa-readme:before { content: "\f4d5"; } @@ -10183,10 +10175,6 @@ readers do not read off random characters that represent icons */ content: "\f7c6"; } -.fa-yandex-international:before { - content: "\f414"; -} - .fa-cc-amex:before { content: "\f1f3"; } diff --git a/resources/static/fontawesome/css/all.min.css b/resources/static/fontawesome/css/all.min.css index df7439bc5..39639ae77 100644 --- a/resources/static/fontawesome/css/all.min.css +++ b/resources/static/fontawesome/css/all.min.css @@ -6,4 +6,4 @@ .fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)} .fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-arrow-turn-right:before,.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"} -.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} \ No newline at end of file +.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} \ No newline at end of file diff --git a/resources/view/404.html b/resources/view/404.html index 734f45370..9cd98b26d 100644 --- a/resources/view/404.html +++ b/resources/view/404.html @@ -1,26 +1,27 @@ - - - - - - + + + + + + + - Application Error - - - - - - + Application Error - - + + + + + + + + +
- +
@@ -46,162 +50,129 @@
+
+

-

404 NOT_FOUND

-

No page here

+

404 NOT_FOUND

+

No page here

+
- \ No newline at end of file + + diff --git a/resources/view/admin/users.html b/resources/view/admin/users.html index 5b772a33a..9d37ef3f2 100644 --- a/resources/view/admin/users.html +++ b/resources/view/admin/users.html @@ -1,21 +1,25 @@ - - + +
-

Users

- +

Users

+ + + - - - - - - - + + + + + + + @@ -24,57 +28,60 @@

Users

-
First nameLast nameDisplay nameEmailRolesRegisteredActiveFirst nameLast nameDisplay nameEmailRolesRegisteredActive
- - - - + + + + @@ -37,19 +42,23 @@
Assigned:
User IDRoleAssignedEndpointUser IDRoleAssignedEndpoint

+ +

-
Attachments:
+
Attachments:
- [[${att.name}]] (open) + [[${att.name}]] + (open)
+

- Edit + Edit
@@ -60,30 +69,37 @@
+ th:replace="~{layout/main::page( + title=${codeTo.isNew() + ? #messages.msg('common.new') + ' ' + objectName + : #messages.msg('common.edit') + ' ' + codeTo.code}, + appMain=~{::appEdit} + )}">
-
+
-
+

- +

+ +
-
Attachments
+
Attachments
@@ -94,36 +110,42 @@
Attachments
- + -
[[${att.name}]] - (open) + + [[${att.name}]] + (open) + + + +
- +
+
- Cancel - + Cancel +
- \ No newline at end of file + diff --git a/resources/view/task-edit.html b/resources/view/task-edit.html index 1b8f5fc0e..5f85a1246 100644 --- a/resources/view/task-edit.html +++ b/resources/view/task-edit.html @@ -7,57 +7,75 @@ - - + + +
- +
+
- +
-
+ + +
+
- +
-
+ +
+
- +
- Comments + Comments
-
+
- +
- +
- +
  • Registration
- + th:field="*{email}"/>
  • Registration
- + th:field="*{password}"/>
  • Registration
- +
@@ -63,28 +68,17 @@

Registration


-
- OR -
+
АБО

-

- Fast registration through social networks +

+ Швидка реєстрація через соціальні мережі

- + - - - - - - @@ -92,7 +86,8 @@

Registration

- Already have an account? Sign in + Вже маєте обліковий запис? + Увійти

diff --git a/schema.pg.sql b/schema.pg.sql new file mode 100644 index 000000000..e8ea1aa46 --- /dev/null +++ b/schema.pg.sql @@ -0,0 +1,799 @@ +-- +-- PostgreSQL database dump +-- + +\restrict CGEKq3GOuJwbHchuUOkz1Kot6z78hel9CeifGTebySL1whooP6IzzanvqWAY8LS + +-- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) +-- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: activity; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.activity ( + id bigint NOT NULL, + author_id bigint NOT NULL, + task_id bigint NOT NULL, + updated timestamp without time zone, + comment character varying(4096), + title character varying(1024), + description character varying(4096), + estimate integer, + type_code character varying(32), + status_code character varying(32), + priority_code character varying(32) +); + + +ALTER TABLE public.activity OWNER TO jira; + +-- +-- Name: activity_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.activity_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.activity_id_seq OWNER TO jira; + +-- +-- Name: activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.activity_id_seq OWNED BY public.activity.id; + + +-- +-- Name: attachment; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.attachment ( + id bigint NOT NULL, + name character varying(128) NOT NULL, + file_link character varying(2048) NOT NULL, + object_id bigint NOT NULL, + object_type smallint NOT NULL, + user_id bigint NOT NULL, + date_time timestamp without time zone +); + + +ALTER TABLE public.attachment OWNER TO jira; + +-- +-- Name: attachment_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.attachment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.attachment_id_seq OWNER TO jira; + +-- +-- Name: attachment_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.attachment_id_seq OWNED BY public.attachment.id; + + +-- +-- Name: contact; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.contact ( + id bigint NOT NULL, + code character varying(32) NOT NULL, + value character varying(256) NOT NULL +); + + +ALTER TABLE public.contact OWNER TO jira; + +-- +-- Name: databasechangelog; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.databasechangelog ( + id character varying(255) NOT NULL, + author character varying(255) NOT NULL, + filename character varying(255) NOT NULL, + dateexecuted timestamp without time zone NOT NULL, + orderexecuted integer NOT NULL, + exectype character varying(10) NOT NULL, + md5sum character varying(35), + description character varying(255), + comments character varying(255), + tag character varying(255), + liquibase character varying(20), + contexts character varying(255), + labels character varying(255), + deployment_id character varying(10) +); + + +ALTER TABLE public.databasechangelog OWNER TO jira; + +-- +-- Name: databasechangeloglock; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.databasechangeloglock ( + id integer NOT NULL, + locked boolean NOT NULL, + lockgranted timestamp without time zone, + lockedby character varying(255) +); + + +ALTER TABLE public.databasechangeloglock OWNER TO jira; + +-- +-- Name: mail_case; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.mail_case ( + id bigint NOT NULL, + email character varying(255) NOT NULL, + name character varying(255) NOT NULL, + date_time timestamp without time zone NOT NULL, + result character varying(255) NOT NULL, + template character varying(255) NOT NULL +); + + +ALTER TABLE public.mail_case OWNER TO jira; + +-- +-- Name: mail_case_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.mail_case_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.mail_case_id_seq OWNER TO jira; + +-- +-- Name: mail_case_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.mail_case_id_seq OWNED BY public.mail_case.id; + + +-- +-- Name: profile; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.profile ( + id bigint NOT NULL, + last_login timestamp without time zone, + last_failed_login timestamp without time zone, + mail_notifications bigint +); + + +ALTER TABLE public.profile OWNER TO jira; + +-- +-- Name: project; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.project ( + id bigint NOT NULL, + code character varying(32) NOT NULL, + title character varying(1024) NOT NULL, + description character varying(4096) NOT NULL, + type_code character varying(32) NOT NULL, + startpoint timestamp without time zone, + endpoint timestamp without time zone, + parent_id bigint +); + + +ALTER TABLE public.project OWNER TO jira; + +-- +-- Name: project_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.project_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.project_id_seq OWNER TO jira; + +-- +-- Name: project_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.project_id_seq OWNED BY public.project.id; + + +-- +-- Name: reference; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.reference ( + id bigint NOT NULL, + code character varying(32) NOT NULL, + ref_type smallint NOT NULL, + endpoint timestamp without time zone, + startpoint timestamp without time zone, + title character varying(1024) NOT NULL, + aux character varying +); + + +ALTER TABLE public.reference OWNER TO jira; + +-- +-- Name: reference_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.reference_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.reference_id_seq OWNER TO jira; + +-- +-- Name: reference_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.reference_id_seq OWNED BY public.reference.id; + + +-- +-- Name: sprint; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.sprint ( + id bigint NOT NULL, + status_code character varying(32) NOT NULL, + startpoint timestamp without time zone, + endpoint timestamp without time zone, + code character varying(32) NOT NULL, + project_id bigint NOT NULL +); + + +ALTER TABLE public.sprint OWNER TO jira; + +-- +-- Name: sprint_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.sprint_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.sprint_id_seq OWNER TO jira; + +-- +-- Name: sprint_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.sprint_id_seq OWNED BY public.sprint.id; + + +-- +-- Name: task; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.task ( + id bigint NOT NULL, + title character varying(1024) NOT NULL, + type_code character varying(32) NOT NULL, + status_code character varying(32) NOT NULL, + project_id bigint NOT NULL, + sprint_id bigint, + parent_id bigint, + startpoint timestamp without time zone, + endpoint timestamp without time zone +); + + +ALTER TABLE public.task OWNER TO jira; + +-- +-- Name: task_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.task_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.task_id_seq OWNER TO jira; + +-- +-- Name: task_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.task_id_seq OWNED BY public.task.id; + + +-- +-- Name: task_tag; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.task_tag ( + task_id bigint NOT NULL, + tag character varying(32) NOT NULL +); + + +ALTER TABLE public.task_tag OWNER TO jira; + +-- +-- Name: user_belong; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.user_belong ( + id bigint NOT NULL, + object_id bigint NOT NULL, + object_type smallint NOT NULL, + user_id bigint NOT NULL, + user_type_code character varying(32) NOT NULL, + startpoint timestamp without time zone, + endpoint timestamp without time zone +); + + +ALTER TABLE public.user_belong OWNER TO jira; + +-- +-- Name: user_belong_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.user_belong_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.user_belong_id_seq OWNER TO jira; + +-- +-- Name: user_belong_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.user_belong_id_seq OWNED BY public.user_belong.id; + + +-- +-- Name: user_role; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.user_role ( + user_id bigint NOT NULL, + role smallint NOT NULL +); + + +ALTER TABLE public.user_role OWNER TO jira; + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: jira +-- + +CREATE TABLE public.users ( + id bigint NOT NULL, + display_name character varying(32) NOT NULL, + email character varying(128) NOT NULL, + first_name character varying(32) NOT NULL, + last_name character varying(32), + password character varying(128) NOT NULL, + endpoint timestamp without time zone, + startpoint timestamp without time zone +); + + +ALTER TABLE public.users OWNER TO jira; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: jira +-- + +CREATE SEQUENCE public.users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.users_id_seq OWNER TO jira; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: jira +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: activity id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.activity ALTER COLUMN id SET DEFAULT nextval('public.activity_id_seq'::regclass); + + +-- +-- Name: attachment id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.attachment ALTER COLUMN id SET DEFAULT nextval('public.attachment_id_seq'::regclass); + + +-- +-- Name: mail_case id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.mail_case ALTER COLUMN id SET DEFAULT nextval('public.mail_case_id_seq'::regclass); + + +-- +-- Name: project id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.project ALTER COLUMN id SET DEFAULT nextval('public.project_id_seq'::regclass); + + +-- +-- Name: reference id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.reference ALTER COLUMN id SET DEFAULT nextval('public.reference_id_seq'::regclass); + + +-- +-- Name: sprint id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.sprint ALTER COLUMN id SET DEFAULT nextval('public.sprint_id_seq'::regclass); + + +-- +-- Name: task id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task ALTER COLUMN id SET DEFAULT nextval('public.task_id_seq'::regclass); + + +-- +-- Name: user_belong id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.user_belong ALTER COLUMN id SET DEFAULT nextval('public.user_belong_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: activity activity_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.activity + ADD CONSTRAINT activity_pkey PRIMARY KEY (id); + + +-- +-- Name: attachment attachment_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.attachment + ADD CONSTRAINT attachment_pkey PRIMARY KEY (id); + + +-- +-- Name: contact contact_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.contact + ADD CONSTRAINT contact_pkey PRIMARY KEY (id, code); + + +-- +-- Name: databasechangeloglock databasechangeloglock_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.databasechangeloglock + ADD CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (id); + + +-- +-- Name: mail_case mail_case_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.mail_case + ADD CONSTRAINT mail_case_pkey PRIMARY KEY (id); + + +-- +-- Name: profile profile_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.profile + ADD CONSTRAINT profile_pkey PRIMARY KEY (id); + + +-- +-- Name: project project_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.project + ADD CONSTRAINT project_pkey PRIMARY KEY (id); + + +-- +-- Name: reference reference_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.reference + ADD CONSTRAINT reference_pkey PRIMARY KEY (id); + + +-- +-- Name: sprint sprint_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.sprint + ADD CONSTRAINT sprint_pkey PRIMARY KEY (id); + + +-- +-- Name: task task_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task + ADD CONSTRAINT task_pkey PRIMARY KEY (id); + + +-- +-- Name: project uk_project_code; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.project + ADD CONSTRAINT uk_project_code UNIQUE (code); + + +-- +-- Name: reference uk_reference_ref_type_code; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.reference + ADD CONSTRAINT uk_reference_ref_type_code UNIQUE (ref_type, code); + + +-- +-- Name: task_tag uk_task_tag; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task_tag + ADD CONSTRAINT uk_task_tag UNIQUE (task_id, tag); + + +-- +-- Name: user_role uk_user_role; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.user_role + ADD CONSTRAINT uk_user_role UNIQUE (user_id, role); + + +-- +-- Name: users uk_users_display_name; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT uk_users_display_name UNIQUE (display_name); + + +-- +-- Name: users uk_users_email; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT uk_users_email UNIQUE (email); + + +-- +-- Name: user_belong user_belong_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.user_belong + ADD CONSTRAINT user_belong_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: ix_user_belong_user_id; Type: INDEX; Schema: public; Owner: jira +-- + +CREATE INDEX ix_user_belong_user_id ON public.user_belong USING btree (user_id); + + +-- +-- Name: uk_sprint_project_code; Type: INDEX; Schema: public; Owner: jira +-- + +CREATE UNIQUE INDEX uk_sprint_project_code ON public.sprint USING btree (project_id, code); + + +-- +-- Name: uk_user_belong; Type: INDEX; Schema: public; Owner: jira +-- + +CREATE UNIQUE INDEX uk_user_belong ON public.user_belong USING btree (object_id, object_type, user_id, user_type_code) WHERE (endpoint IS NULL); + + +-- +-- Name: activity fk_activity_task; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.activity + ADD CONSTRAINT fk_activity_task FOREIGN KEY (task_id) REFERENCES public.task(id) ON DELETE CASCADE; + + +-- +-- Name: activity fk_activity_users; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.activity + ADD CONSTRAINT fk_activity_users FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: attachment fk_attachment; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.attachment + ADD CONSTRAINT fk_attachment FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: contact fk_contact_profile; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.contact + ADD CONSTRAINT fk_contact_profile FOREIGN KEY (id) REFERENCES public.profile(id) ON DELETE CASCADE; + + +-- +-- Name: profile fk_profile_users; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.profile + ADD CONSTRAINT fk_profile_users FOREIGN KEY (id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: project fk_project_parent; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.project + ADD CONSTRAINT fk_project_parent FOREIGN KEY (parent_id) REFERENCES public.project(id) ON DELETE CASCADE; + + +-- +-- Name: sprint fk_sprint_project; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.sprint + ADD CONSTRAINT fk_sprint_project FOREIGN KEY (project_id) REFERENCES public.project(id) ON DELETE CASCADE; + + +-- +-- Name: task fk_task_parent_task; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task + ADD CONSTRAINT fk_task_parent_task FOREIGN KEY (parent_id) REFERENCES public.task(id) ON DELETE CASCADE; + + +-- +-- Name: task fk_task_project; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task + ADD CONSTRAINT fk_task_project FOREIGN KEY (project_id) REFERENCES public.project(id) ON DELETE CASCADE; + + +-- +-- Name: task fk_task_sprint; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task + ADD CONSTRAINT fk_task_sprint FOREIGN KEY (sprint_id) REFERENCES public.sprint(id) ON DELETE SET NULL; + + +-- +-- Name: task_tag fk_task_tag; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.task_tag + ADD CONSTRAINT fk_task_tag FOREIGN KEY (task_id) REFERENCES public.task(id) ON DELETE CASCADE; + + +-- +-- Name: user_belong fk_user_belong; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.user_belong + ADD CONSTRAINT fk_user_belong FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: user_role fk_user_role; Type: FK CONSTRAINT; Schema: public; Owner: jira +-- + +ALTER TABLE ONLY public.user_role + ADD CONSTRAINT fk_user_role FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict CGEKq3GOuJwbHchuUOkz1Kot6z78hel9CeifGTebySL1whooP6IzzanvqWAY8LS + diff --git a/schema_test.pg.sql b/schema_test.pg.sql new file mode 100644 index 000000000..fbee9fea2 --- /dev/null +++ b/schema_test.pg.sql @@ -0,0 +1,26 @@ +-- +-- PostgreSQL database dump +-- + +\restrict IxxkYdoCZj25fw6u8oSRAI4VhiCxeeMbbptbdPG7xol8BsSe1xS1wrXjxxgl8Mw + +-- Dumped from database version 16.10 (Debian 16.10-1.pgdg13+1) +-- Dumped by pg_dump version 16.10 (Debian 16.10-1.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict IxxkYdoCZj25fw6u8oSRAI4VhiCxeeMbbptbdPG7xol8BsSe1xS1wrXjxxgl8Mw + diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java b/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java index 2ddd79591..3670cad10 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/AttachmentController.java @@ -1,72 +1,43 @@ package com.javarush.jira.bugtracking.attachment; -import com.javarush.jira.bugtracking.ObjectType; -import com.javarush.jira.login.AuthUser; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.util.List; - -import static com.javarush.jira.common.BaseHandler.createdResponse; - @RestController -@RequestMapping(value = AttachmentController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +@RequestMapping("/api/attachments") @RequiredArgsConstructor -@Slf4j public class AttachmentController { - static final String REST_URL = "/api/attachments"; - private final AttachmentRepository repository; - @Transactional - @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity upload(@RequestPart MultipartFile file, @RequestParam ObjectType type, - @RequestParam Long objectId, @AuthenticationPrincipal AuthUser authUser) { - log.debug("upload file {} to folder {}", file.getOriginalFilename(), type.toString().toLowerCase()); - String path = FileUtil.getPath(type.toString()); - Attachment attachment = new Attachment(null, path, objectId, type, authUser.id(), file.getOriginalFilename()); - Attachment created = repository.save(attachment); - String fileName = attachment.id() + "_" + file.getOriginalFilename(); - attachment.setFileLink(attachment.getFileLink() + fileName); + @PostMapping("/{type}") + public ResponseEntity upload( + @PathVariable String type, + @RequestPart("file") MultipartFile file) { + + String path = FileUtil.getPath(type); + String fileName = file.getOriginalFilename(); FileUtil.upload(file, path, fileName); - return createdResponse(REST_URL, created); + + return ResponseEntity.ok(path + "/" + fileName); } - @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void delete(@PathVariable Long id) { - log.debug("delete file id = {}", id); - Attachment attachment = repository.getExisted(id); - repository.deleteExisted(id); - FileUtil.delete(attachment.getFileLink()); + @DeleteMapping("/{fileLink}") + public ResponseEntity delete(@PathVariable String fileLink) { + FileUtil.delete(fileLink); + return ResponseEntity.noContent().build(); } - @GetMapping(value = "/download/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity download(@PathVariable long id) { - log.debug("download file id = {}", id); - Attachment attachment = repository.getExisted(id); - Resource resource = FileUtil.download(attachment.getFileLink()); + @GetMapping("/{fileLink}") + public ResponseEntity download(@PathVariable String fileLink) { + Resource resource = FileUtil.download(fileLink); return ResponseEntity.ok() - .header("Content-Disposition", "attachment; filename=" + resource.getFilename()) + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + resource.getFilename() + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } - - @GetMapping("/for-object") - public List getAllForObject(@RequestParam long objectId, @RequestParam ObjectType type) { - log.info("get all attachment by objectId = {}", objectId); - return repository.getAllForObject(objectId, type); - } - - @GetMapping - public List getAll() { - log.info("get all attachment"); - return repository.findAll(); - } } diff --git a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java index 6cffbe175..f3d53f771 100644 --- a/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java +++ b/src/main/java/com/javarush/jira/bugtracking/attachment/FileUtil.java @@ -1,65 +1,69 @@ package com.javarush.jira.bugtracking.attachment; import com.javarush.jira.common.error.IllegalRequestDataException; -import com.javarush.jira.common.error.NotFoundException; -import lombok.experimental.UtilityClass; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.net.MalformedURLException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.io.InputStream; +import java.nio.file.*; +import java.util.Objects; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -@UtilityClass public class FileUtil { - private static final String ATTACHMENT_PATH = "./attachments/%s/"; + + private FileUtil() {} public static void upload(MultipartFile multipartFile, String directoryPath, String fileName) { - if (multipartFile.isEmpty()) { - throw new IllegalRequestDataException("Select a file to upload."); + if (multipartFile == null || multipartFile.isEmpty()) { + throw new IllegalRequestDataException("Empty file"); } + try { + Path dir = Paths.get(directoryPath).toAbsolutePath().normalize(); + Files.createDirectories(dir); - File dir = new File(directoryPath); - if (dir.exists() || dir.mkdirs()) { - File file = new File(directoryPath + fileName); - try (OutputStream outStream = new FileOutputStream(file)) { - outStream.write(multipartFile.getBytes()); - } catch (IOException ex) { - throw new IllegalRequestDataException("Failed to upload file" + multipartFile.getOriginalFilename()); + String safeName = Paths.get( + Objects.requireNonNullElse(fileName, "file") + ).getFileName().toString().trim(); + if (safeName.isEmpty()) { + throw new IllegalRequestDataException("Invalid file name"); } - } - } - public static Resource download(String fileLink) { - Path path = Paths.get(fileLink); - try { - Resource resource = new UrlResource(path.toUri()); - if (resource.exists() || resource.isReadable()) { - return resource; - } else { - throw new IllegalRequestDataException("Failed to download file " + resource.getFilename()); + Path target = dir.resolve(safeName).normalize(); + if (!target.startsWith(dir)) { + throw new IllegalRequestDataException("Path traversal detected"); } - } catch (MalformedURLException ex) { - throw new NotFoundException("File" + fileLink + " not found"); + + try (InputStream in = multipartFile.getInputStream()) { + Files.copy(in, target, REPLACE_EXISTING); + } + } catch (IOException ex) { + throw new IllegalRequestDataException( + "Failed to upload file: " + multipartFile.getOriginalFilename() + ); } } - public static void delete(String fileLink) { - Path path = Paths.get(fileLink); + public static void delete(String filePath) { try { - Files.delete(path); + Path path = Paths.get(filePath).toAbsolutePath().normalize(); + Files.deleteIfExists(path); } catch (IOException ex) { - throw new IllegalRequestDataException("File" + fileLink + " deletion failed."); + throw new IllegalRequestDataException("Failed to delete file: " + filePath); + } + } + + public static Resource download(String filePath) { + Path path = Paths.get(filePath).toAbsolutePath().normalize(); + if (!Files.exists(path)) { + throw new IllegalRequestDataException("File not found: " + filePath); } + return new FileSystemResource(path); } - public static String getPath(String titleType) { - return String.format(ATTACHMENT_PATH, titleType.toLowerCase()); + public static String getPath(String type) { + return Paths.get("uploads", type).toString(); } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java index 7938541bb..b47f1dcf6 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/ActivityService.java @@ -7,6 +7,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Comparator; import java.util.List; @@ -73,4 +76,49 @@ private void updateTaskIfRequired(long taskId, String activityStatus, String act } } } + + private List loadActivitiesAsc(long taskId) { + List list = handler.getRepository().findAllByTaskIdOrderByUpdatedDesc(taskId); + list.removeIf(a -> a.getUpdated() == null); + list.sort(Comparator.comparing(Activity::getUpdated)); + return list; + } + + public Duration getTimeInWork(Task task) { + List acts = loadActivitiesAsc(task.getId()); + Duration total = Duration.ZERO; + LocalDateTime started = null; + + for (Activity a : acts) { + String st = a.getStatusCode(); + if (st == null) continue; + + if ("in_progress".equals(st)) { + started = a.getUpdated(); + } else if ("ready_for_review".equals(st) && started != null) { + total = total.plus(Duration.between(started, a.getUpdated())); + started = null; + } + } + return total; + } + + public Duration getTimeInTesting(Task task) { + List acts = loadActivitiesAsc(task.getId()); + Duration total = Duration.ZERO; + LocalDateTime started = null; + + for (Activity a : acts) { + String st = a.getStatusCode(); + if (st == null) continue; + + if ("ready_for_review".equals(st)) { + started = a.getUpdated(); + } else if ("done".equals(st) && started != null) { + total = total.plus(Duration.between(started, a.getUpdated())); + started = null; + } + } + return total; + } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/Task.java b/src/main/java/com/javarush/jira/bugtracking/task/Task.java index 6c9f4d96e..68e2f123f 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/Task.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/Task.java @@ -1,5 +1,6 @@ package com.javarush.jira.bugtracking.task; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.javarush.jira.bugtracking.project.Project; import com.javarush.jira.bugtracking.sprint.Sprint; import com.javarush.jira.common.HasCode; @@ -25,14 +26,11 @@ @Setter @NoArgsConstructor public class Task extends TitleEntity implements HasCode { - // title, typeCode, statusCode duplicated here and in Activity for sql simplicity - // link to Reference.code with RefType.TASK @Code @Column(name = "type_code", nullable = false) private String typeCode; - // link to Reference.code with RefType.TASK_STATUS @Code @Column(name = "status_code", nullable = false) private String statusCode; @@ -66,9 +64,8 @@ public class Task extends TitleEntity implements HasCode { uniqueConstraints = @UniqueConstraint(columnNames = {"task_id", "tag"}, name = "uk_task_tag")) @Column(name = "tag") @ElementCollection(fetch = FetchType.LAZY) - @JoinColumn() - @OnDelete(action = OnDeleteAction.CASCADE) - private Set<@Size(min = 2, max = 32) String> tags = Set.of(); + @JsonIgnore + private Set<@Size(min = 2, max = 32) String> tags = new java.util.HashSet<>(); // history of comments and task fields changing @OneToMany(mappedBy = "taskId", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java index b53f7ff37..867771e91 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskController.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Set; import static com.javarush.jira.common.BaseHandler.createdResponse; @@ -75,12 +76,20 @@ public List getAllByProject(@RequestParam long projectId) { return handler.getMapper().toToList(handler.getRepository().findAllByProjectId(projectId)); } + @GetMapping("/{taskId}/tags") + public Set listTags(@PathVariable long taskId) { + return taskService.getTaskTags(taskId); + } + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createWithLocation(@Valid @RequestBody TaskToExt taskTo) { - return createdResponse(REST_URL, taskService.create(taskTo)); + public ResponseEntity createWithLocation(@Valid @RequestBody TaskToExt taskTo) { + Task created = taskService.create(taskTo); + TaskToFull dto = taskService.get(created.id()); + return createdResponse(REST_URL, dto); } + @PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) public void update(@Valid @RequestBody TaskToExt taskTo, @PathVariable long id) { diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java index 982c4be8b..934979b87 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskRepository.java @@ -3,10 +3,12 @@ import com.javarush.jira.common.BaseRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; +import java.util.Set; @Transactional(readOnly = true) public interface TaskRepository extends BaseRepository { @@ -22,6 +24,9 @@ public interface TaskRepository extends BaseRepository { @Query("SELECT t FROM Task t JOIN FETCH t.project LEFT JOIN FETCH t.sprint LEFT JOIN FETCH t.parent WHERE t.id =:id") Optional findFullById(long id); + @Query("select distinct tag from Task t join t.tags tag where t.id = :taskId") + Set findTagsByTaskId(@Param("taskId") long taskId); + @Modifying @Query(value = """ WITH RECURSIVE task_with_subtasks AS ( diff --git a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java index e6f385548..05b1a2332 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/TaskService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; + import java.util.*; import java.time.LocalDateTime; import java.util.List; @@ -39,6 +40,7 @@ public class TaskService { private final SprintRepository sprintRepository; private final TaskExtMapper extMapper; private final UserBelongRepository userBelongRepository; + private final TaskRepository taskRepository; @Transactional public void changeStatus(long taskId, String statusCode) { @@ -86,8 +88,10 @@ public void update(TaskToExt taskTo, long id) { } } + @Transactional(readOnly = true) public TaskToFull get(long id) { Task task = Util.checkExist(id, handler.getRepository().findFullById(id)); + task.getTags().size(); TaskToFull taskToFull = fullMapper.toTo(task); List activities = activityHandler.getRepository().findAllByTaskIdOrderByUpdatedDesc(id); fillExtraFields(taskToFull, activities); @@ -140,4 +144,55 @@ private void checkAssignmentActionPossible(long id, String userType, boolean ass throw new DataConflictException(String.format(assign ? CANNOT_ASSIGN : CANNOT_UN_ASSIGN, userType, task.getStatusCode())); } } + + @Transactional + public void addTagsToTask(long taskId, java.util.Set names) { + Task task = handler.getRepository().getExisted(taskId); + if (task.getTags() == null) { + task.setTags(new java.util.HashSet<>()); + } else if (!(task.getTags() instanceof java.util.HashSet)) { + task.setTags(new java.util.HashSet<>(task.getTags())); + } + task.getTags().addAll(clean(names)); + } + + @Transactional(readOnly = true) + public Set getTaskTags(long taskId) { + return taskRepository.findTagsByTaskId(taskId); + } + + + @Transactional + public void setTaskTags(long taskId, java.util.Set names) { + Task task = handler.getRepository().getExisted(taskId); + java.util.Set cleaned = clean(names); + if (task.getTags() == null) { + task.setTags(new java.util.HashSet<>(cleaned)); + } else { + task.getTags().clear(); + task.getTags().addAll(cleaned); + } + } + + @Transactional + public void removeTagFromTask(long taskId, String tagName) { + Task task = handler.getRepository().getExisted(taskId); + if (task.getTags() == null || tagName == null) return; + String trimmed = tagName.trim(); + if (trimmed.isEmpty()) return; + + if (!(task.getTags() instanceof java.util.HashSet)) { + task.setTags(new java.util.HashSet<>(task.getTags())); + } + task.getTags().remove(trimmed); + } + + private static java.util.Set clean(java.util.Set in) { + if (in == null) return java.util.Set.of(); + return in.stream() + .filter(java.util.Objects::nonNull) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new)); + } } diff --git a/src/main/java/com/javarush/jira/bugtracking/task/internal/web/TaskTagRestController.java b/src/main/java/com/javarush/jira/bugtracking/task/internal/web/TaskTagRestController.java new file mode 100644 index 000000000..edf64969b --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/internal/web/TaskTagRestController.java @@ -0,0 +1,44 @@ +package com.javarush.jira.bugtracking.task.internal.web; + +import com.javarush.jira.bugtracking.task.TaskService; +import com.javarush.jira.bugtracking.task.to.TaskTagsTo; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Set; + +@RestController +@RequestMapping("/api/tasks/{taskId}/tags") +@PreAuthorize("hasAnyRole('ADMIN','USER')") +public class TaskTagRestController { + private final TaskService taskService; + + public TaskTagRestController(TaskService taskService) { + this.taskService = taskService; + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void addMany(@PathVariable long taskId, @RequestBody @Valid TaskTagsTo to) { + taskService.addTagsToTask(taskId, to.names()); + } + + @PutMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void replaceAll(@PathVariable long taskId, @RequestBody @Valid TaskTagsTo to) { + taskService.setTaskTags(taskId, to.names()); + } + + @DeleteMapping("/{name}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteOne(@PathVariable long taskId, @PathVariable String name) { + taskService.removeTagFromTask(taskId, name); + } + + @GetMapping + public Set list(@PathVariable long taskId) { + return taskService.get(taskId).getTags(); + } +} diff --git a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java index dfbd9858e..5c1f87c67 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/mapper/TaskFullMapper.java @@ -5,10 +5,13 @@ import com.javarush.jira.common.BaseMapper; import com.javarush.jira.common.TimestampMapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; @Mapper(config = TimestampMapper.class) public interface TaskFullMapper extends BaseMapper { @Override + @Mapping(target = "tags", source = "tags") TaskToFull toTo(Task task); } + diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskTagsTo.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskTagsTo.java new file mode 100644 index 000000000..7a51a8d79 --- /dev/null +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskTagsTo.java @@ -0,0 +1,8 @@ +package com.javarush.jira.bugtracking.task.to; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import java.util.Set; + +public record TaskTagsTo(@NotEmpty Set<@Size(min = 2, max = 32) String> names) {} + diff --git a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java index 090d48ef0..6ab70f00d 100644 --- a/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java +++ b/src/main/java/com/javarush/jira/bugtracking/task/to/TaskToFull.java @@ -6,17 +6,23 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Getter public class TaskToFull extends TaskToExt { CodeTo parent; CodeTo project; CodeTo sprint; + @Setter List activityTos; + @Setter + Set tags; + public TaskToFull(Long id, String code, String title, String description, String typeCode, String statusCode, String priorityCode, - LocalDateTime updated, Integer estimate, CodeTo parent, CodeTo project, CodeTo sprint, List activityTos) { + LocalDateTime updated, Integer estimate, CodeTo parent, CodeTo project, CodeTo sprint, + List activityTos) { super(id, code, title, description, typeCode, statusCode, priorityCode, updated, estimate, parent == null ? null : parent.getId(), project.getId(), sprint == null ? null : sprint.getId()); this.parent = parent; diff --git a/src/main/java/com/javarush/jira/common/internal/config/I18nConfig.java b/src/main/java/com/javarush/jira/common/internal/config/I18nConfig.java new file mode 100644 index 000000000..0f2fb81b6 --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/config/I18nConfig.java @@ -0,0 +1,43 @@ +package com.javarush.jira.common.internal.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.CookieLocaleResolver; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import java.time.Duration; +import java.util.Locale; + +@Configuration +public class I18nConfig implements WebMvcConfigurer { + + @Bean + public CookieLocaleResolver localeResolver() { + CookieLocaleResolver r = new CookieLocaleResolver(); + r.setCookieName("LANG"); + r.setDefaultLocale(new Locale("uk")); + r.setCookieMaxAge((int) Duration.ofDays(365).getSeconds()); + r.setCookiePath("/"); + return r; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor i = new LocaleChangeInterceptor(); + i.setParamName("lang"); + return i; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()) + .addPathPatterns("/**"); + } + + @Override + public void addViewControllers(org.springframework.web.servlet.config.annotation.ViewControllerRegistry registry) { + registry.addViewController("/view/login").setViewName("view/login"); + } + +} \ No newline at end of file diff --git a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java index f500f27a0..2f6f2ab8e 100644 --- a/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java +++ b/src/main/java/com/javarush/jira/common/internal/config/SecurityConfig.java @@ -5,8 +5,10 @@ import com.javarush.jira.login.internal.UserRepository; import com.javarush.jira.login.internal.sociallogin.CustomOAuth2UserService; import com.javarush.jira.login.internal.sociallogin.CustomTokenResponseConverter; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -70,31 +72,40 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { @Bean @Order(2) - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests() - .requestMatchers("/view/unauth/**", "/ui/register/**", "/ui/password/**").anonymous() - .requestMatchers("/", "/doc", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/static/**").permitAll() - .requestMatchers("/ui/admin/**", "/view/admin/**").hasRole(Role.ADMIN.name()) - .requestMatchers("/ui/mngr/**").hasAnyRole(Role.ADMIN.name(), Role.MANAGER.name()) - .anyRequest().authenticated() - .and().formLogin().permitAll() - .loginPage("/view/login") - .defaultSuccessUrl("/", true) - .and().oauth2Login() - .loginPage("/view/login") - .defaultSuccessUrl("/", true) - .tokenEndpoint() - .accessTokenResponseClient(accessTokenResponseClient()) - .and() - .userInfoEndpoint() - .userService(customOAuth2UserService) - .and().and().logout() - .logoutUrl("/ui/logout") - .logoutSuccessUrl("/") - .invalidateHttpSession(true) - .clearAuthentication(true) - .deleteCookies("JSESSIONID") - .and().csrf().disable(); + public SecurityFilterChain filterChain(HttpSecurity http, + ObjectProvider clientRegs) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/view/unauth/**", "/ui/register/**", "/ui/password/**").anonymous() + .requestMatchers("/", "/doc", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/static/**").permitAll() + .requestMatchers("/i18n/**").permitAll() + .requestMatchers("/ui/admin/**", "/view/admin/**").hasRole(Role.ADMIN.name()) + .requestMatchers("/ui/mngr/**").hasAnyRole(Role.ADMIN.name(), Role.MANAGER.name()) + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/view/login") + .permitAll() + .defaultSuccessUrl("/", true) + ); + + if (clientRegs.getIfAvailable() != null) { + http.oauth2Login(oauth -> oauth + .loginPage("/view/login") + .defaultSuccessUrl("/", true) + .tokenEndpoint(token -> token.accessTokenResponseClient(accessTokenResponseClient())) + .userInfoEndpoint(user -> user.userService(customOAuth2UserService)) + ); + } + + http.logout(logout -> logout + .logoutUrl("/ui/logout") + .logoutSuccessUrl("/") + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID") + ) + .csrf(csrf -> csrf.disable()); + return http.build(); } diff --git a/src/main/java/com/javarush/jira/common/internal/i18n/LocaleController.java b/src/main/java/com/javarush/jira/common/internal/i18n/LocaleController.java new file mode 100644 index 000000000..9b5715c14 --- /dev/null +++ b/src/main/java/com/javarush/jira/common/internal/i18n/LocaleController.java @@ -0,0 +1,27 @@ +package com.javarush.jira.common.internal.i18n; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.support.RequestContextUtils; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Locale; + +@Controller +public class LocaleController { + + @GetMapping("/i18n/set") + public String setLang(@RequestParam("lang") String lang, + HttpServletRequest request, + HttpServletResponse response) { + LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request); + if (resolver != null) { + Locale target = Locale.forLanguageTag(lang); + resolver.setLocale(request, response, target); + } + return "redirect:/view/login"; + } +} diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java deleted file mode 100644 index e8e05be05..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/VkOAuth2UserDataHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; - -@Component("vk") -public class VkOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return getAttribute(oAuth2UserData, "last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("email"); - } - - private String getAttribute(OAuth2UserData oAuth2UserData, String name) { - List> attributesResponse = oAuth2UserData.getData("response"); - if (attributesResponse != null) { - Map attributes = attributesResponse.get(0); - if (attributes != null) { - return (String) attributes.get(name); - } - } - return null; - } -} diff --git a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java b/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java deleted file mode 100644 index e8ea1ac1d..000000000 --- a/src/main/java/com/javarush/jira/login/internal/sociallogin/handler/YandexOAuth2UserDataHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.javarush.jira.login.internal.sociallogin.handler; - -import org.springframework.stereotype.Component; - -@Component("yandex") -public class YandexOAuth2UserDataHandler implements OAuth2UserDataHandler { - @Override - public String getFirstName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("first_name"); - } - - @Override - public String getLastName(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("last_name"); - } - - @Override - public String getEmail(OAuth2UserData oAuth2UserData) { - return oAuth2UserData.getData("default_email"); - } -} diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 000000000..f45394142 --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,35 @@ +app: + host-url: ${APP_HOST_URL:http://localhost:8080} + test-mail: ${APP_TEST_MAIL:jira4jr@gmail.com} + +server: + port: 8080 + address: 0.0.0.0 + servlet: + context-path: / + +spring: + main: + web-application-type: servlet + + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: org.postgresql.Driver + + liquibase: + change-log: classpath:db/changelog.sql + sql: + init: + mode: never + + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration + +spring.security.oauth2.client: + registration: {} + provider: {} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7fcba1570..c90c3bf56 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,7 +1,6 @@ -# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html app: - host-url: http://localhost:8080 - test-mail: jira4jr@gmail.com + host-url: ${APP_HOST_URL:http://localhost:8080} + test-mail: ${APP_TEST_MAIL:jira4jr@gmail.com} templates-update-cache: 5s mail-sending-props: core-pool-size: 8 @@ -10,30 +9,33 @@ app: spring: init: mode: never + + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: org.postgresql.Driver + jpa: show-sql: true open-in-view: false - - # validate db by model hibernate: ddl-auto: validate - properties: - # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations hibernate: format_sql: true default_batch_fetch_size: 20 - # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc jdbc.batch_size: 20 - datasource: - url: jdbc:postgresql://localhost:5432/jira - username: jira - password: JiraRush + jdbc: + time_zone: UTC liquibase: - changeLog: "classpath:db/changelog.sql" + change-log: classpath:db/changelog.sql + + messages: + basename: messages + encoding: UTF-8 - # Jackson Fields Serialization jackson: visibility: field: any @@ -41,83 +43,57 @@ spring: setter: none is-getter: none - # https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties-cache cache: cache-names: users - caffeine.spec: maximumSize=10000,expireAfterAccess=5m - - security: - oauth2: - client: - registration: - github: - client-id: 3d0d8738e65881fff266 - client-secret: 0f97031ce6178b7dfb67a6af587f37e222a16120 - scope: - - email - google: - client-id: 329113642700-f8if6pu68j2repq3ef6umd5jgiliup60.apps.googleusercontent.com - client-secret: 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 - scope: - - email - - profile - vk: - client-id: 51562377 - client-secret: jNM1YHQy1362Mqs49wUN - client-name: Vkontakte - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - client-authentication-method: client_secret_post - authorization-grant-type: authorization_code - scope: email - yandex: - client-id: 2f3395214ba84075956b76a34b231985 - client-secret: ed236c501e444a609b0f419e5e88f1e1 - client-name: Yandex - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code - gitlab: - client-id: b8520a3266089063c0d8261cce36971defa513f5ffd9f9b7a3d16728fc83a494 - client-secret: e72c65320cf9d6495984a37b0f9cc03ec46be0bb6f071feaebbfe75168117004 - client-name: GitLab - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - authorization-grant-type: authorization_code - scope: read_user - provider: - vk: - authorization-uri: https://oauth.vk.com/authorize - token-uri: https://oauth.vk.com/access_token - user-info-uri: https://api.vk.com/method/users.get?v=8.1 - user-name-attribute: response - yandex: - authorization-uri: https://oauth.yandex.ru/authorize - token-uri: https://oauth.yandex.ru/token - user-info-uri: https://login.yandex.ru/info - user-name-attribute: login - gitlab: - authorization-uri: https://gitlab.com/oauth/authorize - token-uri: https://gitlab.com/oauth/token - user-info-uri: https://gitlab.com/api/v4/user - user-name-attribute: email + caffeine: + spec: maximumSize=10000,expireAfterAccess=5m sql: init: - mode: always + mode: never mail: + host: ${MAIL_HOST:smtp.gmail.com} + port: ${MAIL_PORT:587} + username: ${MAIL_USER} + password: ${MAIL_PASSWORD} properties: mail: smtp: + auth: true starttls: enable: true - auth: true - host: smtp.gmail.com - username: jira4jr@gmail.com - password: zdfzsrqvgimldzyj - port: 587 - thymeleaf.check-template-location: false - mvc.throw-exception-if-no-handler-found: true - web.resources.add-mappings: false + thymeleaf: + check-template-location: false + + mvc: + throw-exception-if-no-handler-found: true + + web: + resources: + add-mappings: false + + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration + + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:} + client-secret: ${GOOGLE_CLIENT_SECRET:} + github: + client-id: ${GITHUB_CLIENT_ID:} + client-secret: ${GITHUB_CLIENT_SECRET:} + gitlab: + client-id: ${GITLAB_CLIENT_ID:} + client-secret: ${GITLAB_CLIENT_SECRET:} + provider: {} logging: level: @@ -127,11 +103,13 @@ logging: org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver: DEBUG server: - # https://springdoc.org/index.html#how-can-i-deploy-springdoc-openapi-ui-behind-a-reverse-proxy forward-headers-strategy: framework servlet: encoding: - charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly - enabled: true # Enable http encoding support + charset: UTF-8 + enabled: true force: true -springdoc.swagger-ui.path: /doc + +springdoc: + swagger-ui: + path: /doc diff --git a/src/main/resources/data4dev/data.sql b/src/main/resources/data4dev/data.sql index a7d43cbad..28b494155 100644 --- a/src/main/resources/data4dev/data.sql +++ b/src/main/resources/data4dev/data.sql @@ -54,8 +54,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); delete from ATTACHMENT; diff --git a/src/main/resources/db/changelog.sql b/src/main/resources/db/changelog.sql index 68591336d..d0c51669d 100644 --- a/src/main/resources/db/changelog.sql +++ b/src/main/resources/db/changelog.sql @@ -1,331 +1,365 @@ ---liquibase formatted sql - ---changeset kmpk:init_schema -DROP TABLE IF EXISTS USER_ROLE; -DROP TABLE IF EXISTS CONTACT; -DROP TABLE IF EXISTS MAIL_CASE; -DROP -SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; -DROP TABLE IF EXISTS PROFILE; -DROP TABLE IF EXISTS TASK_TAG; -DROP TABLE IF EXISTS USER_BELONG; -DROP -SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; -DROP TABLE IF EXISTS ACTIVITY; -DROP -SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; -DROP TABLE IF EXISTS TASK; -DROP -SEQUENCE IF EXISTS TASK_ID_SEQ; -DROP TABLE IF EXISTS SPRINT; -DROP -SEQUENCE IF EXISTS SPRINT_ID_SEQ; -DROP TABLE IF EXISTS PROJECT; -DROP -SEQUENCE IF EXISTS PROJECT_ID_SEQ; -DROP TABLE IF EXISTS REFERENCE; -DROP -SEQUENCE IF EXISTS REFERENCE_ID_SEQ; -DROP TABLE IF EXISTS ATTACHMENT; -DROP -SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; -DROP TABLE IF EXISTS USERS; -DROP -SEQUENCE IF EXISTS USERS_ID_SEQ; - -create table PROJECT -( - ID bigserial primary key, - CODE varchar(32) not null - constraint UK_PROJECT_CODE unique, - TITLE varchar(1024) not null, - DESCRIPTION varchar(4096) not null, - TYPE_CODE varchar(32) not null, - STARTPOINT timestamp, - ENDPOINT timestamp, - PARENT_ID bigint, - constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade -); - -create table MAIL_CASE -( - ID bigserial primary key, - EMAIL varchar(255) not null, - NAME varchar(255) not null, - DATE_TIME timestamp not null, - RESULT varchar(255) not null, - TEMPLATE varchar(255) not null -); - -create table SPRINT -( - ID bigserial primary key, - STATUS_CODE varchar(32) not null, - STARTPOINT timestamp, - ENDPOINT timestamp, - TITLE varchar(1024) not null, - PROJECT_ID bigint not null, - constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade -); - -create table REFERENCE -( - ID bigserial primary key, - CODE varchar(32) not null, - REF_TYPE smallint not null, - ENDPOINT timestamp, - STARTPOINT timestamp, - TITLE varchar(1024) not null, - AUX varchar, - constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) -); - -create table USERS -( - ID bigserial primary key, - DISPLAY_NAME varchar(32) not null - constraint UK_USERS_DISPLAY_NAME unique, - EMAIL varchar(128) not null - constraint UK_USERS_EMAIL unique, - FIRST_NAME varchar(32) not null, - LAST_NAME varchar(32), - PASSWORD varchar(128) not null, - ENDPOINT timestamp, - STARTPOINT timestamp -); - -create table PROFILE -( - ID bigint primary key, - LAST_LOGIN timestamp, - LAST_FAILED_LOGIN timestamp, - MAIL_NOTIFICATIONS bigint, - constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade -); - -create table CONTACT -( - ID bigint not null, - CODE varchar(32) not null, - VALUE varchar(256) not null, - primary key (ID, CODE), - constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade -); - -create table TASK -( - ID bigserial primary key, - TITLE varchar(1024) not null, - DESCRIPTION varchar(4096) not null, - TYPE_CODE varchar(32) not null, - STATUS_CODE varchar(32) not null, - PRIORITY_CODE varchar(32) not null, - ESTIMATE integer, - UPDATED timestamp, - PROJECT_ID bigint not null, - SPRINT_ID bigint, - PARENT_ID bigint, - STARTPOINT timestamp, - ENDPOINT timestamp, - constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, - constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, - constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade -); - -create table ACTIVITY -( - ID bigserial primary key, - AUTHOR_ID bigint not null, - TASK_ID bigint not null, - UPDATED timestamp, - COMMENT varchar(4096), --- history of task field change - TITLE varchar(1024), - DESCRIPTION varchar(4096), - ESTIMATE integer, - TYPE_CODE varchar(32), - STATUS_CODE varchar(32), - PRIORITY_CODE varchar(32), - constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), - constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade -); - -create table TASK_TAG -( - TASK_ID bigint not null, - TAG varchar(32) not null, - constraint UK_TASK_TAG unique (TASK_ID, TAG), - constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade -); - -create table USER_BELONG -( - ID bigserial primary key, - OBJECT_ID bigint not null, - OBJECT_TYPE smallint not null, - USER_ID bigint not null, - USER_TYPE_CODE varchar(32) not null, - STARTPOINT timestamp, - ENDPOINT timestamp, - constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) -); -create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); -create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); - -create table ATTACHMENT -( - ID bigserial primary key, - NAME varchar(128) not null, - FILE_LINK varchar(2048) not null, - OBJECT_ID bigint not null, - OBJECT_TYPE smallint not null, - USER_ID bigint not null, - DATE_TIME timestamp, - constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) -); - -create table USER_ROLE -( - USER_ID bigint not null, - ROLE smallint not null, - constraint UK_USER_ROLE unique (USER_ID, ROLE), - constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade -); - ---changeset kmpk:populate_data ---============ References ================= -insert into REFERENCE (CODE, TITLE, REF_TYPE) --- TASK -values ('task', 'Task', 2), - ('story', 'Story', 2), - ('bug', 'Bug', 2), - ('epic', 'Epic', 2), --- SPRINT_STATUS - ('planning', 'Planning', 4), - ('active', 'Active', 4), - ('finished', 'Finished', 4), --- USER_TYPE - ('author', 'Author', 5), - ('developer', 'Developer', 5), - ('reviewer', 'Reviewer', 5), - ('tester', 'Tester', 5), --- PROJECT - ('scrum', 'Scrum', 1), - ('task_tracker', 'Task tracker', 1), --- CONTACT - ('skype', 'Skype', 0), - ('tg', 'Telegram', 0), - ('mobile', 'Mobile', 0), - ('phone', 'Phone', 0), - ('website', 'Website', 0), - ('vk', 'VK', 0), - ('linkedin', 'LinkedIn', 0), - ('github', 'GitHub', 0), --- PRIORITY - ('critical', 'Critical', 7), - ('high', 'High', 7), - ('normal', 'Normal', 7), - ('low', 'Low', 7), - ('neutral', 'Neutral', 7); - -insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) --- MAIL_NOTIFICATION -values ('assigned', 'Assigned', 6, '1'), - ('three_days_before_deadline', 'Three days before deadline', 6, '2'), - ('two_days_before_deadline', 'Two days before deadline', 6, '4'), - ('one_day_before_deadline', 'One day before deadline', 6, '8'), - ('deadline', 'Deadline', 6, '16'), - ('overdue', 'Overdue', 6, '32'), --- TASK_STATUS - ('todo', 'ToDo', 3, 'in_progress,canceled'), - ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), - ('ready_for_review', 'Ready for review', 3, 'review,canceled'), - ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), - ('ready_for_test', 'Ready for test', 3, 'test,canceled'), - ('test', 'Test', 3, 'done,in_progress,canceled'), - ('done', 'Done', 3, 'canceled'), - ('canceled', 'Canceled', 3, null); - ---changeset gkislin:change_backtracking_tables - -alter table SPRINT rename COLUMN TITLE to CODE; -alter table SPRINT - alter column CODE type varchar (32); -alter table SPRINT - alter column CODE set not null; -create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); - -ALTER TABLE TASK - DROP COLUMN DESCRIPTION; -ALTER TABLE TASK - DROP COLUMN PRIORITY_CODE; -ALTER TABLE TASK - DROP COLUMN ESTIMATE; -ALTER TABLE TASK - DROP COLUMN UPDATED; - ---changeset ishlyakhtenkov:change_task_status_reference - -delete -from REFERENCE -where REF_TYPE = 3; -insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) -values ('todo', 'ToDo', 3, 'in_progress,canceled'), - ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), - ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), - ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), - ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), - ('test', 'Test', 3, 'done,in_progress,canceled'), - ('done', 'Done', 3, 'canceled'), - ('canceled', 'Canceled', 3, null); - ---changeset gkislin:users_add_on_delete_cascade - -alter table ACTIVITY - drop constraint FK_ACTIVITY_USERS, - add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; - -alter table USER_BELONG - drop constraint FK_USER_BELONG, - add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; - -alter table ATTACHMENT - drop constraint FK_ATTACHMENT, - add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; - ---changeset valeriyemelyanov:change_user_type_reference - -delete -from REFERENCE -where REF_TYPE = 5; -insert into REFERENCE (CODE, TITLE, REF_TYPE) --- USER_TYPE -values ('project_author', 'Author', 5), - ('project_manager', 'Manager', 5), - ('sprint_author', 'Author', 5), - ('sprint_manager', 'Manager', 5), - ('task_author', 'Author', 5), - ('task_developer', 'Developer', 5), - ('task_reviewer', 'Reviewer', 5), - ('task_tester', 'Tester', 5); - ---changeset apolik:refactor_reference_aux - --- TASK_TYPE -delete -from REFERENCE -where REF_TYPE = 3; -insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) -values ('todo', 'ToDo', 3, 'in_progress,canceled|'), - ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), - ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), - ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), - ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), - ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), - ('done', 'Done', 3, 'canceled|'), - ('canceled', 'Canceled', 3, null); - ---changeset ishlyakhtenkov:change_UK_USER_BELONG - -drop index UK_USER_BELONG; -create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE) where ENDPOINT is null; +--liquibase formatted sql + +--changeset kmpk:init_schema +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP +SEQUENCE IF EXISTS MAIL_CASE_ID_SEQ; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS USER_BELONG; +DROP +SEQUENCE IF EXISTS USER_BELONG_ID_SEQ; +DROP TABLE IF EXISTS ACTIVITY; +DROP +SEQUENCE IF EXISTS ACTIVITY_ID_SEQ; +DROP TABLE IF EXISTS TASK; +DROP +SEQUENCE IF EXISTS TASK_ID_SEQ; +DROP TABLE IF EXISTS SPRINT; +DROP +SEQUENCE IF EXISTS SPRINT_ID_SEQ; +DROP TABLE IF EXISTS PROJECT; +DROP +SEQUENCE IF EXISTS PROJECT_ID_SEQ; +DROP TABLE IF EXISTS REFERENCE; +DROP +SEQUENCE IF EXISTS REFERENCE_ID_SEQ; +DROP TABLE IF EXISTS ATTACHMENT; +DROP +SEQUENCE IF EXISTS ATTACHMENT_ID_SEQ; +DROP TABLE IF EXISTS USERS; +DROP +SEQUENCE IF EXISTS USERS_ID_SEQ; + +create table PROJECT +( + ID bigserial primary key, + CODE varchar(32) not null + constraint UK_PROJECT_CODE unique, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + PARENT_ID bigint, + constraint FK_PROJECT_PARENT foreign key (PARENT_ID) references PROJECT (ID) on delete cascade +); + +create table MAIL_CASE +( + ID bigserial primary key, + EMAIL varchar(255) not null, + NAME varchar(255) not null, + DATE_TIME timestamp not null, + RESULT varchar(255) not null, + TEMPLATE varchar(255) not null +); + +create table SPRINT +( + ID bigserial primary key, + STATUS_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + TITLE varchar(1024) not null, + PROJECT_ID bigint not null, + constraint FK_SPRINT_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade +); + +create table REFERENCE +( + ID bigserial primary key, + CODE varchar(32) not null, + REF_TYPE smallint not null, + ENDPOINT timestamp, + STARTPOINT timestamp, + TITLE varchar(1024) not null, + AUX varchar, + constraint UK_REFERENCE_REF_TYPE_CODE unique (REF_TYPE, CODE) +); + +create table USERS +( + ID bigserial primary key, + DISPLAY_NAME varchar(32) not null + constraint UK_USERS_DISPLAY_NAME unique, + EMAIL varchar(128) not null + constraint UK_USERS_EMAIL unique, + FIRST_NAME varchar(32) not null, + LAST_NAME varchar(32), + PASSWORD varchar(128) not null, + ENDPOINT timestamp, + STARTPOINT timestamp +); + +create table PROFILE +( + ID bigint primary key, + LAST_LOGIN timestamp, + LAST_FAILED_LOGIN timestamp, + MAIL_NOTIFICATIONS bigint, + constraint FK_PROFILE_USERS foreign key (ID) references USERS (ID) on delete cascade +); + +create table CONTACT +( + ID bigint not null, + CODE varchar(32) not null, + VALUE varchar(256) not null, + primary key (ID, CODE), + constraint FK_CONTACT_PROFILE foreign key (ID) references PROFILE (ID) on delete cascade +); + +create table TASK +( + ID bigserial primary key, + TITLE varchar(1024) not null, + DESCRIPTION varchar(4096) not null, + TYPE_CODE varchar(32) not null, + STATUS_CODE varchar(32) not null, + PRIORITY_CODE varchar(32) not null, + ESTIMATE integer, + UPDATED timestamp, + PROJECT_ID bigint not null, + SPRINT_ID bigint, + PARENT_ID bigint, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_TASK_SPRINT foreign key (SPRINT_ID) references SPRINT (ID) on delete set null, + constraint FK_TASK_PROJECT foreign key (PROJECT_ID) references PROJECT (ID) on delete cascade, + constraint FK_TASK_PARENT_TASK foreign key (PARENT_ID) references TASK (ID) on delete cascade +); + +create table ACTIVITY +( + ID bigserial primary key, + AUTHOR_ID bigint not null, + TASK_ID bigint not null, + UPDATED timestamp, + COMMENT varchar(4096), +-- history of task field change + TITLE varchar(1024), + DESCRIPTION varchar(4096), + ESTIMATE integer, + TYPE_CODE varchar(32), + STATUS_CODE varchar(32), + PRIORITY_CODE varchar(32), + constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID), + constraint FK_ACTIVITY_TASK foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table TASK_TAG +( + TASK_ID bigint not null, + TAG varchar(32) not null, + constraint UK_TASK_TAG unique (TASK_ID, TAG), + constraint FK_TASK_TAG foreign key (TASK_ID) references TASK (ID) on delete cascade +); + +create table USER_BELONG +( + ID bigserial primary key, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + USER_TYPE_CODE varchar(32) not null, + STARTPOINT timestamp, + ENDPOINT timestamp, + constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) +); +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE); +create index IX_USER_BELONG_USER_ID on USER_BELONG (USER_ID); + +create table ATTACHMENT +( + ID bigserial primary key, + NAME varchar(128) not null, + FILE_LINK varchar(2048) not null, + OBJECT_ID bigint not null, + OBJECT_TYPE smallint not null, + USER_ID bigint not null, + DATE_TIME timestamp, + constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) +); + +create table USER_ROLE +( + USER_ID bigint not null, + ROLE smallint not null, + constraint UK_USER_ROLE unique (USER_ID, ROLE), + constraint FK_USER_ROLE foreign key (USER_ID) references USERS (ID) on delete cascade +); + +--changeset kmpk:populate_data +--============ References ================= +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- TASK +values ('task', 'Task', 2), + ('story', 'Story', 2), + ('bug', 'Bug', 2), + ('epic', 'Epic', 2), +-- SPRINT_STATUS + ('planning', 'Planning', 4), + ('active', 'Active', 4), + ('finished', 'Finished', 4), +-- USER_TYPE + ('author', 'Author', 5), + ('developer', 'Developer', 5), + ('reviewer', 'Reviewer', 5), + ('tester', 'Tester', 5), +-- PROJECT + ('scrum', 'Scrum', 1), + ('task_tracker', 'Task tracker', 1), +-- CONTACT + ('skype', 'Skype', 0), + ('tg', 'Telegram', 0), + ('mobile', 'Mobile', 0), + ('phone', 'Phone', 0), + ('website', 'Website', 0), + ('vk', 'VK', 0), + ('linkedin', 'LinkedIn', 0), + ('github', 'GitHub', 0), +-- PRIORITY + ('critical', 'Critical', 7), + ('high', 'High', 7), + ('normal', 'Normal', 7), + ('low', 'Low', 7), + ('neutral', 'Neutral', 7); + +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +-- MAIL_NOTIFICATION +values ('assigned', 'Assigned', 6, '1'), + ('three_days_before_deadline', 'Three days before deadline', 6, '2'), + ('two_days_before_deadline', 'Two days before deadline', 6, '4'), + ('one_day_before_deadline', 'One day before deadline', 6, '8'), + ('deadline', 'Deadline', 6, '16'), + ('overdue', 'Overdue', 6, '32'), +-- TASK_STATUS + ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:change_backtracking_tables + +alter table SPRINT rename COLUMN TITLE to CODE; +alter table SPRINT + alter column CODE type varchar (32); +alter table SPRINT + alter column CODE set not null; +create unique index UK_SPRINT_PROJECT_CODE on SPRINT (PROJECT_ID, CODE); + +ALTER TABLE TASK + DROP COLUMN DESCRIPTION; +ALTER TABLE TASK + DROP COLUMN PRIORITY_CODE; +ALTER TABLE TASK + DROP COLUMN ESTIMATE; +ALTER TABLE TASK + DROP COLUMN UPDATED; + +--changeset ishlyakhtenkov:change_task_status_reference + +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled'), + ('test', 'Test', 3, 'done,in_progress,canceled'), + ('done', 'Done', 3, 'canceled'), + ('canceled', 'Canceled', 3, null); + +--changeset gkislin:users_add_on_delete_cascade + +alter table ACTIVITY + drop constraint FK_ACTIVITY_USERS, + add constraint FK_ACTIVITY_USERS foreign key (AUTHOR_ID) references USERS (ID) on delete cascade; + +alter table USER_BELONG + drop constraint FK_USER_BELONG, + add constraint FK_USER_BELONG foreign key (USER_ID) references USERS (ID) on delete cascade; + +alter table ATTACHMENT + drop constraint FK_ATTACHMENT, + add constraint FK_ATTACHMENT foreign key (USER_ID) references USERS (ID) on delete cascade; + +--changeset valeriyemelyanov:change_user_type_reference + +delete +from REFERENCE +where REF_TYPE = 5; +insert into REFERENCE (CODE, TITLE, REF_TYPE) +-- USER_TYPE +values ('project_author', 'Author', 5), + ('project_manager', 'Manager', 5), + ('sprint_author', 'Author', 5), + ('sprint_manager', 'Manager', 5), + ('task_author', 'Author', 5), + ('task_developer', 'Developer', 5), + ('task_reviewer', 'Reviewer', 5), + ('task_tester', 'Tester', 5); + +--changeset apolik:refactor_reference_aux + +-- TASK_TYPE +delete +from REFERENCE +where REF_TYPE = 3; +insert into REFERENCE (CODE, TITLE, REF_TYPE, AUX) +values ('todo', 'ToDo', 3, 'in_progress,canceled|'), + ('in_progress', 'In progress', 3, 'ready_for_review,canceled|task_developer'), + ('ready_for_review', 'Ready for review', 3, 'in_progress,review,canceled|'), + ('review', 'Review', 3, 'in_progress,ready_for_test,canceled|task_reviewer'), + ('ready_for_test', 'Ready for test', 3, 'review,test,canceled|'), + ('test', 'Test', 3, 'done,in_progress,canceled|task_tester'), + ('done', 'Done', 3, 'canceled|'), + ('canceled', 'Canceled', 3, null); + +--changeset ishlyakhtenkov:change_UK_USER_BELONG + +drop index UK_USER_BELONG; +create unique index UK_USER_BELONG on USER_BELONG (OBJECT_ID, OBJECT_TYPE, USER_ID, USER_TYPE_CODE) where ENDPOINT is null; + +--changeset nastya:seed_duration_demo runOnChange:true + +-- seed: project, task, user +insert into PROJECT (ID, CODE, TITLE, TYPE_CODE, DESCRIPTION, PARENT_ID) +values (91001, 'PRJ_SEED', 'Seed Project', 'software', 'Project for duration demo', null) +on conflict (id) do nothing; + +insert into TASK (ID, TITLE, TYPE_CODE, STATUS_CODE, PROJECT_ID) +values (92001, 'Seed Task for duration', 'task', 'done', 91001) +on conflict (id) do nothing; + +insert into USERS (ID, EMAIL, PASSWORD, FIRST_NAME, LAST_NAME, DISPLAY_NAME) +values (93001, 'admin@gmail.com', '{noop}admin', 'Admin', 'Admin', 'admin') +on conflict (id) do nothing; + +insert into USER_ROLE (USER_ID, ROLE) +values (93001, 1) +on conflict do nothing; + +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE) +values (93001, 92001, timestamp '2025-08-20 10:00:00','Status: In progress', null, null, 'task', 'in_progress'); + +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE) +values (93001, 92001, timestamp '2025-08-21 16:30:00','Status: Ready for review', null, null, 'task', 'ready_for_review'); + +insert into ACTIVITY (AUTHOR_ID, TASK_ID, UPDATED, TITLE, DESCRIPTION, ESTIMATE, TYPE_CODE, STATUS_CODE) +values (93001, 92001, timestamp '2025-08-22 12:15:00','Status: Done', null, null, 'task', 'done'); + +--rollback delete from ACTIVITY where TASK_ID=92001; +--rollback delete from USER_ROLE where USER_ID=93001; +--rollback delete from USERS where ID=93001; +--rollback delete from TASK where ID=92001; +--rollback delete from PROJECT where ID=91001; diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml deleted file mode 100644 index 29cd2bb97..000000000 --- a/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - logs/jirarush.log - - %d{MM-dd HH:mm:ss.SSS} [%8.8thread] %-5.5level| %class{60}.%M:%L %msg%n - - - logs/jirarush.%d{yyyy-MM-dd}.log - 30 - - - - - - - \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 000000000..db0c9c97b --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,269 @@ +login.title=Sign in +login.username=E-mail +login.password=Password +login.button=Sign in +login.error=Invalid username or password +login.forgot=Forgot your password? +login.or=OR +login.newhere=New here? +login.social.title=Sign in with social networks +login.sso.title=Sign in with social networks +login.sso.google=Sign in with Google +login.sso.github=Sign in with GitHub +login.sso.gitlab=Sign in with GitLab +login.new=New here? +login.create=Create account + +tree.title=Tree +tree.createProject=Create new Project + +sprint.status.label=Status: +sprint.project.label=Project: +sprint.report.link=Report +sprint.dashboard.link=Dashboard +sprint.status.planned=Planned +sprint.status.active=Active +sprint.status.finished=Finished +sprint.status.disabled=Disabled +sprint.status.placeholder=Select status + +datetime.pattern.long=MM/dd/yyyy, HH:mm:ss +datetime.pattern.short=dd.MM.yyyy, HH:mm + +task.description.label=Description: +task.priority.label=Priority: +task.status.label=Status: +task.type.label=Type: +task.updated.label=Updated: +task.estimate.label=Estimate: +task.parent.label=Parent: +task.project.label=Project: +task.sprint.label=Sprint: +task.comments.title=Comments +task.history.title=History log +task.history.author=Author +task.history.time=Time +task.history.status=Status +task.history.priority=Priority +task.history.type=Type +task.history.title.col=Title +task.history.estimate=Estimate + +task.status.open=Open +task.status.in_progress=In progress +task.status.ready_for_review=Ready for review +task.status.testing=Testing +task.status.done=Done +task.status.blocked=Blocked + +task.priority.low=Low +task.priority.medium=Medium +task.priority.high=High +task.priority.critical=Critical + +task.type.task=Task +task.type.bug=Bug +task.type.feature=Feature +task.type.story=Story + +task.comments.add=Add comment +task.comments.edit=Edit comment +task.comments.delete=Delete comment +task.comments.save=Save comment +task.comments.placeholder=Write a comment... + +users.title=Users +users.add=Add +users.firstName=First name +users.firstName.placeholder=First name +users.lastName=Last name +users.lastName.placeholder=Last name +users.displayName=Display name +users.displayName.placeholder=Display name +users.email=Email +users.email.placeholder=Email +users.password=Password +users.password.placeholder=Password +users.roles=Roles +users.registered=Registered +users.active=Active +users.modal.title=Edit user + +reports.page.title=JiraRush: mini bugtracking system +reports.title=Reports +reports.project=Project +reports.sprint=Sprint +reports.noProjects=No projects +reports.status=Status +reports.started=Started +reports.completed=Completed +reports.duration=Duration + +reports.taskSummaries=Task summaries + +register.title=Registration +register.firstName=First name +register.lastName=Last name +register.email=E-mail +register.password=Password +register.button=Create account +register.or=OR +register.social=Fast registration through social networks +register.haveAccount=Already have an account? +register.signin=Sign in + +references.title=References +references.add=+ Add reference +references.col.title=Title +references.col.code=Code +references.col.aux=AUX +references.col.type=Type +references.modal.title=Reference + +project.edit.type=Type +project.description=Description +project.type.product=Product +project.type.service=Service +project.type.internal=Internal +project.type.research=Research + +project.parent.label=Parent project: +project.description.empty=No description + +profile.title=Profile +profile.userInfo=User info +profile.changePassword=Change password +profile.oldPassword=Old password +profile.newPassword=New password +profile.email=Email +profile.firstName=First name +profile.lastName=Last name +profile.displayName=Display name +profile.mailNotifications=Mail notifications +profile.contacts=Contacts +profile.contact.type=Type +profile.contact.value=Contact +profile.addContact=Add contact + +mail.reset.title=JiraRush — set a new password +mail.reset.hello=Hello, {0}. +mail.reset.request=We received a request to set a new JiraRush password for the account: {0}. +mail.reset.button=Set password +mail.reset.note=If you didn’t request this, please ignore this email. +mail.reset.signature=Best regards, the JiraRush team + +error404.title=Application Error +error404.meta.title=404 Not found +error404.meta.description=404 Not found +error404.code=404 NOT_FOUND +error404.message=No page here + +header.search.placeholder=Search +header.swagger=Swagger API +header.profile=Profile + +sidebar.link1=Link_1 +sidebar.link2=Link_2 +sidebar.link3=Link_3 +sidebar.link4=Link_4 +sidebar.link5=Link_5 + +footer.learn=LEARN +footer.learn.programming=Programming Courses +footer.learn.java=Java Course +footer.learn.help=Help with Tasks +footer.learn.pricing=Pricing +footer.learn.games=Game Projects + +footer.community=COMMUNITY +footer.community.users=Users +footer.community.articles=Articles +footer.community.forum=Forum +footer.community.chat=Chat +footer.community.success=Success Stories + +footer.company=COMPANY +footer.company.about=About us +footer.company.contacts=Contacts +footer.company.reviews=Reviews +footer.company.faq=FAQ +footer.company.support=Support + +footer.about=JavaRush is an online course for learning Java programming from scratch. This course is a perfect way to master Java for beginners. It contains 1200+ tasks with instant verification and an essential scope of Java fundamentals theory. +footer.follow=FOLLOW US +footer.copyright=Programmers Are Made, Not Born © 2023 JavaRush + +common.close=Close +common.cancel=Cancel +common.save=Save +common.view=View +common.new=New +common.edit=Edit +common.title=Title +common.code=Code +common.enabled=Enabled +common.assigned=Assigned: +common.userId=User ID +common.role=Role +common.assigned.at=Assigned +common.endpoint=Endpoint +common.attachments=Attachments +common.open=open +common.upload=+ Upload attachment +common.confirmEnable=Are you sure to {0} [[${codeTo.code}]]? +common.continue=Continue +common.or=OR + +password.reset.title=Set new password +password.reset.choose=Choose a new password +password.reset.placeholder=Enter new password + +dashboard.page.title=JiraRush: mini bugtracking system +dashboard.title=Dashboard +dashboard.project=Project +dashboard.noProjects=No projects +dashboard.col.todo=TO DO +dashboard.col.inprogress=IN PROGRESS +dashboard.col.review=REVIEW +dashboard.col.test=TEST +dashboard.col.done=DONE + +mail.confirm.title=JiraRush - email confirmation +mail.confirm.hello=Hello, {0}. +mail.confirm.body=To complete setting up your account and start using JiraRush, please confirm your email address. +mail.confirm.button=Confirm email +mail.confirm.note=If you didn’t sign up for JiraRush, just ignore this email. +mail.confirm.signature=Best regards, the JiraRush team + +error.page.title=Application Error + +password.recovery.title=Password recovery +password.recovery.header=Password recovery +password.recovery.instruction=We'll send a recovery link to +password.recovery.email.placeholder=Enter email +password.recovery.backToLogin=Return to log in +password.recovery.send=Send email + +header.loginRegister=Login/Register +header.logout=Logout +header.loginReg.short=Login/Reg + +sidebar.tree=Tree +sidebar.dashboard=Dashboard +sidebar.reports=Reports +sidebar.users=Users +sidebar.references=References + +home.page.title=JiraRush: mini bugtracking system +home.page.header=JiraRush Home page +home.page.logout=Logout + + + + + + + + + + diff --git a/src/main/resources/messages_uk.properties b/src/main/resources/messages_uk.properties new file mode 100644 index 000000000..bf11541cd --- /dev/null +++ b/src/main/resources/messages_uk.properties @@ -0,0 +1,266 @@ +login.title=Вхід +login.username=Е-мейл +login.password=Пароль +login.button=Увійти +login.error=Невірний е-мейл або пароль +login.social.title=Увійти через соцмережі +login.newhere=Вперше тут? +login.or=АБО +login.forgot=Забули пароль? +login.sso.title=Увійдіть через соціальні мережі +login.sso.google=Увійти через Google +login.sso.github=Увійти через GitHub +login.sso.gitlab=Увійти через GitLab +login.new=Вперше тут? +login.create=Створити обліковий запис + +tree.title=Дерево +tree.createProject=Створити новий проєкт + +sprint.status.label=Статус: +sprint.project.label=Проєкт: +sprint.report.link=Звіт +sprint.dashboard.link=Дашборд +sprint.status.planned=Заплановано +sprint.status.active=Активний +sprint.status.finished=Завершений +sprint.status.disabled=Вимкнений +sprint.status.placeholder=Оберіть статус + +task.description.label=Опис: +task.priority.label=Пріоритет: +task.status.label=Статус: +task.type.label=Тип: +task.updated.label=Оновлено: +task.estimate.label=Оцінка: +task.parent.label=Батьківська: +task.project.label=Проєкт: +task.sprint.label=Спринт: + +task.comments.title=Коментарі +task.history.title=Історія змін +task.history.author=Автор +task.history.time=Час +task.history.status=Статус +task.history.priority=Пріоритет +task.history.type=Тип +task.history.title.col=Заголовок +task.history.estimate=Оцінка + +task.status.open=Відкрита +task.status.in_progress=В роботі +task.status.ready_for_review=Готово до ревʼю +task.status.testing=Тестування +task.status.done=Завершена +task.status.blocked=Заблокована + +task.priority.low=Низький +task.priority.medium=Середній +task.priority.high=Високий +task.priority.critical=Критичний + +task.type.task=Завдання +task.type.bug=Баг +task.type.feature=Фіча +task.type.story=Історія + +task.comments.add=Додати коментар +task.comments.edit=Редагувати коментар +task.comments.delete=Видалити коментар +task.comments.save=Зберегти коментар +task.comments.placeholder=Напишіть коментар... +common.close=Закрити + +datetime.pattern.long=dd.MM.yyyy, HH:mm:ss +datetime.pattern.short=dd.MM.yyyy, HH:mm + +users.title=Користувачі +users.add=Додати + +users.firstName=Ім'я +users.firstName.placeholder=Ім'я +users.lastName=Прізвище +users.lastName.placeholder=Прізвище +users.displayName=Показуване ім'я +users.displayName.placeholder=Введіть показуване ім'я +users.email=Електронна пошта +users.email.placeholder=Електронна пошта +users.password=Пароль +users.password.placeholder=Пароль +users.roles=Ролі +users.registered=Зареєстрований +users.active=Активний + +users.modal.title=Редагування користувача + +common.cancel=Скасувати +common.save=Зберегти + +reports.page.title=JiraRush: міні система багтрекінгу +reports.title=Звіти + +reports.project=Проєкт +reports.sprint=Спринт +reports.noProjects=Немає проєктів + +reports.status=Статус +reports.started=Початок +reports.completed=Завершено +reports.duration=Тривалість + +reports.taskSummaries=Підсумки задач + +register.title=Реєстрація +register.firstName=Ім’я +register.lastName=Прізвище +register.email=E-mail +register.password=Пароль +register.button=Створити обліковий запис +register.or=АБО +register.social=Швидка реєстрація через соціальні мережі +register.haveAccount=Вже маєте обліковий запис? +register.signin=Увійти + +references.title=Довідники +references.add=+ Додати довідник +references.col.title=Назва +references.col.code=Код +references.col.aux=Додатково +references.col.type=Тип +references.modal.title=Довідник + +project.edit.type=Тип +project.description=Опис + +project.type.product=Продукт +project.type.service=Сервіс +project.type.internal=Внутрішній +project.type.research=Дослідження + +project.parent.label=Батьківський проєкт: +project.description.empty=Опис відсутній + +profile.title=Профіль +profile.userInfo=Інформація користувача +profile.changePassword=Змінити пароль +profile.oldPassword=Старий пароль +profile.newPassword=Новий пароль +profile.email=Ел. пошта +profile.firstName=Ім’я +profile.lastName=Прізвище +profile.displayName=Відображуване ім’я +profile.mailNotifications=Сповіщення поштою +profile.contacts=Контакти +profile.contact.type=Тип +profile.contact.value=Контакт +profile.addContact=Додати контакт + +mail.reset.title=JiraRush — встановити новий пароль +mail.reset.hello=Привіт, {0}. +mail.reset.request=Ми отримали запит на встановлення нового пароля JiraRush для облікового запису: {0}. +mail.reset.button=Встановити пароль +mail.reset.note=Якщо ви не надсилали цей запит, просто проігноруйте цей лист. +mail.reset.signature=З повагою, команда JiraRush + +error404.title=Помилка застосунку +error404.meta.title=404 Не знайдено +error404.meta.description=404 Не знайдено +error404.code=404 НЕ_ЗНАЙДЕНО +error404.message=Сторінку не знайдено + +header.search.placeholder=Пошук +header.swagger=Swagger API +header.profile=Профіль + +sidebar.link1=Посилання_1 +sidebar.link2=Посилання_2 +sidebar.link3=Посилання_3 +sidebar.link4=Посилання_4 +sidebar.link5=Посилання_5 + +footer.learn=НАВЧАННЯ +footer.learn.programming=Курси програмування +footer.learn.java=Курс Java +footer.learn.help=Допомога із завданнями +footer.learn.pricing=Тарифи +footer.learn.games=Ігрові проєкти + +footer.community=СПІЛЬНОТА +footer.community.users=Користувачі +footer.community.articles=Статті +footer.community.forum=Форум +footer.community.chat=Чат +footer.community.success=Історії успіху + +footer.company=КОМПАНІЯ +footer.company.about=Про нас +footer.company.contacts=Контакти +footer.company.reviews=Відгуки +footer.company.faq=Питання-Відповіді +footer.company.support=Підтримка + +footer.about=JavaRush — це онлайн-курс для вивчення Java з нуля. Курс містить 1200+ завдань із миттєвою перевіркою та необхідний обсяг теорії з основ Java. +footer.follow=МИ В СОЦМЕРЕЖАХ +footer.copyright=Програмістами стають, а не народжуються © 2023 JavaRush +footer.rights=Всі права захищені. + +common.view=Перегляд +common.new=Новий +common.edit=Редагувати +common.title=Назва +common.code=Код +common.enabled=Активний +common.assigned=Призначені: +common.userId=ID користувача +common.role=Роль +common.assigned.at=Призначено +common.endpoint=Закінчення +common.attachments=Вкладення +common.open=відкрити +common.upload=+ Завантажити вкладення +common.confirmEnable=Ви впевнені, що хочете {0} [[${codeTo.code}]]? + +password.reset.title=Встановити новий пароль +password.reset.choose=Оберіть новий пароль +password.reset.placeholder=Введіть новий пароль +common.continue=Продовжити + +dashboard.page.title=JiraRush: міні-багтрекінг система +dashboard.title=Дошка +dashboard.project=Проєкт +dashboard.noProjects=Немає проєктів +dashboard.col.todo=У ПЛАНАХ +dashboard.col.inprogress=В РОБОТІ +dashboard.col.review=ПЕРЕВІРКА +dashboard.col.test=ТЕСТ +dashboard.col.done=ГОТОВО + +mail.confirm.title=JiraRush — підтвердження пошти +mail.confirm.hello=Привіт, {0}. +mail.confirm.body=Щоб завершити налаштування облікового запису та почати користуватися JiraRush, підтвердьте правильність вашої електронної пошти. +mail.confirm.button=Підтвердити пошту +mail.confirm.note=Якщо ви не реєструвались у JiraRush, просто проігноруйте цей лист. +mail.confirm.signature=З повагою, команда JiraRush + +error.page.title=Помилка застосунку + +password.recovery.title=Відновлення пароля +password.recovery.header=Відновлення пароля +password.recovery.instruction=Ми надішлемо посилання для відновлення на +password.recovery.email.placeholder=Введіть email +password.recovery.backToLogin=Повернутись до входу +password.recovery.send=Надіслати листа +common.or=АБО + +header.loginReg.short=Вхід/Реєстр + +sidebar.tree=Дерево +sidebar.dashboard=Дошка +sidebar.reports=Звіти +sidebar.users=Користувачі +sidebar.references=Довідники + +home.page.title=JiraRush: міні-багтрекінг система +home.page.header=Головна сторінка JiraRush +home.page.logout=Вийти + diff --git a/src/test/java/com/javarush/jira/AbstractControllerTest.java b/src/test/java/com/javarush/jira/AbstractControllerTest.java index 5981bae53..62a78dea5 100644 --- a/src/test/java/com/javarush/jira/AbstractControllerTest.java +++ b/src/test/java/com/javarush/jira/AbstractControllerTest.java @@ -2,16 +2,22 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications -@Sql(scripts = {"classpath:db/changelog.sql", "classpath:data.sql"}, config = @SqlConfig(encoding = "UTF-8")) -@AutoConfigureMockMvc //https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Sql( + scripts = {"classpath:schema.sql", "classpath:db/data-test.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD +) public abstract class AbstractControllerTest extends BaseTests { @Autowired diff --git a/src/test/java/com/javarush/jira/BaseTests.java b/src/test/java/com/javarush/jira/BaseTests.java index b6ed2d1aa..8a5990c8c 100644 --- a/src/test/java/com/javarush/jira/BaseTests.java +++ b/src/test/java/com/javarush/jira/BaseTests.java @@ -2,8 +2,13 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; @SpringBootTest @ActiveProfiles("test") +@Sql( + scripts = {"classpath:schema.sql", "classpath:db/data-test.sql"}, + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD +) abstract class BaseTests { } diff --git a/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilSmokeTest.java b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilSmokeTest.java new file mode 100644 index 000000000..1f9ebfd01 --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/attachment/FileUtilSmokeTest.java @@ -0,0 +1,37 @@ +package com.javarush.jira.bugtracking.attachment; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.core.io.Resource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.*; + +import static org.junit.jupiter.api.Assertions.*; + +class FileUtilSmokeTest { + + @Test + void uploadDownloadDeleteCycle() throws Exception { + String type = "test-files"; + String dir = FileUtil.getPath(type); + String fileName = "hello.txt"; + String content = "Hello FileUtil " + System.nanoTime(); + + MockMultipartFile mock = new MockMultipartFile( + "file", fileName, "text/plain", content.getBytes(StandardCharsets.UTF_8) + ); + FileUtil.upload(mock, dir, fileName); + + Path uploaded = Paths.get(dir).resolve(fileName).toAbsolutePath(); + assertTrue(Files.exists(uploaded), "файл має існувати після upload"); + + Resource resource = FileUtil.download(uploaded.toString()); + assertTrue(resource.exists(), "ресурс існує"); + String readBack = Files.readString(resource.getFile().toPath()); + assertEquals(content, readBack, "вміст має співпадати"); + + FileUtil.delete(uploaded.toString()); + assertFalse(Files.exists(uploaded), "файл має бути видалений"); + } +} diff --git a/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceDurationTest.java b/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceDurationTest.java new file mode 100644 index 000000000..aa3324709 --- /dev/null +++ b/src/test/java/com/javarush/jira/bugtracking/task/ActivityServiceDurationTest.java @@ -0,0 +1,108 @@ +package com.javarush.jira.bugtracking.task; + +import com.javarush.jira.bugtracking.project.Project; +import com.javarush.jira.bugtracking.project.ProjectRepository; +import com.javarush.jira.login.Role; +import com.javarush.jira.login.User; +import com.javarush.jira.login.internal.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class ActivityServiceDurationTest { + + @Autowired private ActivityService activityService; + @Autowired private TaskRepository taskRepository; + @Autowired private ActivityRepository activityRepository; + @Autowired private UserRepository userRepository; + @Autowired private ProjectRepository projectRepository; + + @PersistenceContext + private EntityManager em; + + @Test + void calculatesWorkAndTestingDurations() { + User author = userRepository.save( + new User(null, "author@test.local", "{noop}pass", + "Author", "Tester", "author", Role.ADMIN) + ); + + Project p = projectRepository.save( + new Project( + null, + "PRJ_TEST_" + System.nanoTime(), + "Test Project", + "software", + "Project for duration test", + null + ) + ); + em.flush(); + assertThat(p.getId()).as("project id must be assigned").isNotNull(); + + Task task = new Task(); + task.setTitle("Duration demo"); + task.setTypeCode("task"); + task.setStatusCode("done"); + task.setProjectId(p.getId()); + task = taskRepository.save(task); + em.flush(); + + LocalDateTime t1 = LocalDateTime.of(2025, 8, 20, 10, 0); + LocalDateTime t2 = LocalDateTime.of(2025, 8, 21, 16, 30); + LocalDateTime t3 = LocalDateTime.of(2025, 8, 22, 12, 15); + + Activity a1 = activityRepository.save(new Activity( + null, author.getId(), task.getId(), t1, + null, "in_progress", null, task.getTypeCode(), + "Status: In progress", null, null + )); + + Activity a2 = activityRepository.save(new Activity( + null, author.getId(), task.getId(), t2, + null, "ready_for_review", null, task.getTypeCode(), + "Status: Ready for review", null, null + )); + + Activity a3 = activityRepository.save(new Activity( + null, author.getId(), task.getId(), t3, + null, "done", null, task.getTypeCode(), + "Status: Done", null, null + )); + em.flush(); + + em.createNativeQuery("update activity set updated = ? where id = ?") + .setParameter(1, Timestamp.valueOf(t1)) + .setParameter(2, a1.getId()) + .executeUpdate(); + em.createNativeQuery("update activity set updated = ? where id = ?") + .setParameter(1, Timestamp.valueOf(t2)) + .setParameter(2, a2.getId()) + .executeUpdate(); + em.createNativeQuery("update activity set updated = ? where id = ?") + .setParameter(1, Timestamp.valueOf(t3)) + .setParameter(2, a3.getId()) + .executeUpdate(); + em.flush(); + em.clear(); + + Duration work = activityService.getTimeInWork(task); + Duration testing = activityService.getTimeInTesting(task); + + assertThat(work).isEqualTo(Duration.ofHours(30).plusMinutes(30)); // 20.08 10:00 -> 21.08 16:30 + assertThat(testing).isEqualTo(Duration.ofHours(19).plusMinutes(45)); // 21.08 16:30 -> 22.08 12:15 + } +} diff --git a/src/test/java/com/javarush/jira/config/TestUserDetailsService.java b/src/test/java/com/javarush/jira/config/TestUserDetailsService.java new file mode 100644 index 000000000..c2167cf59 --- /dev/null +++ b/src/test/java/com/javarush/jira/config/TestUserDetailsService.java @@ -0,0 +1,20 @@ +package com.javarush.jira.config; + +import com.javarush.jira.login.AuthUser; +import com.javarush.jira.login.internal.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.userdetails.UserDetailsService; + +@TestConfiguration +@RequiredArgsConstructor +public class TestUserDetailsService { + + private final UserRepository userRepository; + + @Bean + public UserDetailsService testUserDetailsService() { + return email -> new AuthUser(userRepository.getExistedByEmail(email)); + } +} diff --git a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java index d6790ff3b..0d1326ead 100644 --- a/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java +++ b/src/test/java/com/javarush/jira/login/internal/web/UserControllerTest.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java index a6fd5e3bf..7896a4b01 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileRestControllerTest.java @@ -1,8 +1,160 @@ package com.javarush.jira.profile.internal.web; -import com.javarush.jira.AbstractControllerTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.javarush.jira.login.AuthUser; +import com.javarush.jira.login.Role; +import com.javarush.jira.login.User; +import com.javarush.jira.profile.ContactTo; +import com.javarush.jira.profile.ProfileTo; +import com.javarush.jira.profile.internal.ProfileMapper; +import com.javarush.jira.profile.internal.ProfileRepository; +import com.javarush.jira.profile.internal.model.Profile; +import com.javarush.jira.ref.RefType; +import com.javarush.jira.ref.RefTo; +import com.javarush.jira.ref.ReferenceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.MessageSource; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.web.servlet.MockMvc; +import java.lang.reflect.Field; +import java.util.*; -class ProfileRestControllerTest extends AbstractControllerTest { +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -} \ No newline at end of file +@SpringBootTest +@AutoConfigureMockMvc +class ProfileRestControllerIT { + + private static final long TEST_USER_ID = 2L; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ProfileRepository profileRepository; + + @MockBean + private ProfileMapper profileMapper; + + @MockBean + private MessageSource messageSource; + + @BeforeEach + void setupSecurityContext() throws Exception { + User user = new User(TEST_USER_ID, "admin@gmail.com", "{noop}password", "Admin", "Admin", "Admin", Role.ADMIN); + AuthUser authUser = new AuthUser(user); + Authentication authentication = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities()); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + when(messageSource.getMessage(anyString(), any(), anyString(), any())).thenReturn("Mocked validation message"); + when(messageSource.getMessage(any(), any())).thenReturn("Mocked validation message"); + + Field field = ReferenceService.class.getDeclaredField("refSelect"); + field.setAccessible(true); + Map contactMap = Map.of( + "telegram", new RefTo(1L, RefType.CONTACT, "telegram", "Telegram", null) + ); + Map> allRefs = Map.of(RefType.CONTACT, contactMap); + field.set(null, allRefs); + } + + private ContactTo createValidContact() { + ContactTo contact = new ContactTo(); + contact.setId(TEST_USER_ID); + contact.setCode("telegram"); + contact.setValue("tg://user123"); + return contact; + } + + private ProfileTo createValidProfileTo() { + ProfileTo to = new ProfileTo(TEST_USER_ID, Set.of("ACTIVITY"), Set.of(createValidContact())); + to.setId(TEST_USER_ID); + return to; + } + + @Test + @DisplayName("GET /api/profile - success") + void getProfile_success() throws Exception { + Profile mockProfile = new Profile(); + ProfileTo profileTo = createValidProfileTo(); + + when(profileRepository.getOrCreate(anyLong())).thenReturn(mockProfile); + when(profileMapper.toTo(mockProfile)).thenReturn(profileTo); + + mockMvc.perform(get("/api/profile")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.mailNotifications").value(hasSize(1))) + .andExpect(jsonPath("$.contacts").value(hasSize(1))); + } + + @Test + @DisplayName("GET /api/profile - unauthorized") + void getProfile_unauthorized() throws Exception { + SecurityContextHolder.clearContext(); + mockMvc.perform(get("/api/profile")) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("PUT /api/profile - success") + void updateProfile_success() throws Exception { + ProfileTo updated = createValidProfileTo(); + Profile existing = new Profile(); + Profile mapped = new Profile(); + + when(profileRepository.getOrCreate(TEST_USER_ID)).thenReturn(existing); + when(profileMapper.updateFromTo(any(Profile.class), any(ProfileTo.class))).thenReturn(mapped); + + mockMvc.perform(put("/api/profile") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updated))) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("PUT /api/profile - invalid: mailNotifications has blank value") + void updateProfile_invalid_blankMailNotification() throws Exception { + ContactTo contact = createValidContact(); + ProfileTo invalid = new ProfileTo(TEST_USER_ID, Set.of(""), Set.of(contact)); + invalid.setId(TEST_USER_ID); + + mockMvc.perform(put("/api/profile") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalid))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @DisplayName("PUT /api/profile - invalid: contact value is blank") + void updateProfile_invalid_blankContactValue() throws Exception { + ContactTo contact = createValidContact(); + contact.setValue(""); + ProfileTo invalid = new ProfileTo(TEST_USER_ID, Set.of("ACTIVITY"), Set.of(contact)); + invalid.setId(TEST_USER_ID); + + mockMvc.perform(put("/api/profile") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalid))) + .andExpect(status().isUnprocessableEntity()); + } +} diff --git a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java index fb4407268..cc0513971 100644 --- a/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java +++ b/src/test/java/com/javarush/jira/profile/internal/web/ProfileTestData.java @@ -44,7 +44,6 @@ public static ProfileTo getUpdatedTo() { new ContactTo("website", "new.com"), new ContactTo("github", "newGitHub"), new ContactTo("tg", "newTg"), - new ContactTo("vk", "newVk"), new ContactTo("linkedin", "newLinkedin"))); } @@ -57,7 +56,6 @@ public static Profile getUpdated(long id) { new Contact(id, "website", "new.com"), new Contact(id, "github", "newGitHub"), new Contact(id, "tg", "newTg"), - new Contact(id, "vk", "newVk"), new Contact(id, "linkedin", "newLinkedin"))); return profile; } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 51137fd06..92aae1e50 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,8 +1,64 @@ -spring.cache.type: none +MAIL_USER: test +MAIL_PASSWORD: test + spring: - init: - mode: always datasource: - url: jdbc:postgresql://localhost:5433/jira-test - username: jira - password: JiraRush \ No newline at end of file + url: jdbc:h2:mem:jira_${random.uuid};MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + driver-class-name: org.h2.Driver + + jpa: + show-sql: true + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + format_sql: true + hbm2ddl.auto: none + + liquibase: + enabled: true + change-log: classpath:db/changelog-test.sql + + sql: + init: + mode: never + # mode: always + # schema-locations: classpath:schema.sql + # data-locations: classpath:db/data-test.sql + + test: + database: + replace: NONE + + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration + - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration + + mail: + host: localhost + port: 2525 + username: ${MAIL_USER} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: false + starttls: + enable: false + test-connection: false + + security: + oauth2: + client: + registration: {} + provider: {} + +logging: + level: + org.springframework.jdbc.datasource.init: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 5087dbddc..bc26161e4 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -53,8 +53,7 @@ values (1, 'skype', 'userSkype'), (1, 'mobile', '+01234567890'), (1, 'website', 'user.com'), (2, 'github', 'adminGitHub'), - (2, 'tg', 'adminTg'), - (2, 'vk', 'adminVk'); + (2, 'tg', 'adminTg'); insert into PROJECT (code, title, description, type_code, parent_id) diff --git a/src/test/resources/db/changelog-test.sql b/src/test/resources/db/changelog-test.sql new file mode 100644 index 000000000..cd99f8d21 --- /dev/null +++ b/src/test/resources/db/changelog-test.sql @@ -0,0 +1,200 @@ +SET MODE PostgreSQL; + +SET REFERENTIAL_INTEGRITY FALSE; + +DROP TABLE IF EXISTS activity; +DROP TABLE IF EXISTS attachment; +DROP TABLE IF EXISTS contact; +DROP TABLE IF EXISTS mail_case; +DROP TABLE IF EXISTS profile; +DROP TABLE IF EXISTS project; +DROP TABLE IF EXISTS reference; +DROP TABLE IF EXISTS sprint; +DROP TABLE IF EXISTS task; +DROP TABLE IF EXISTS task_tag; +DROP TABLE IF EXISTS user_belong; +DROP TABLE IF EXISTS user_role; +DROP TABLE IF EXISTS users; + +SET REFERENTIAL_INTEGRITY TRUE; + +-- ========================= +-- Таблиці +-- ========================= +CREATE TABLE users ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + display_name VARCHAR(32) NOT NULL, + email VARCHAR(128) NOT NULL, + first_name VARCHAR(32) NOT NULL, + last_name VARCHAR(32), + password VARCHAR(255) NOT NULL +); + +CREATE TABLE profile ( + id BIGINT NOT NULL, + last_failed_login TIMESTAMP, + last_login TIMESTAMP, + mail_notifications BIGINT +); + +CREATE TABLE contact ( + code VARCHAR(255) NOT NULL, + id BIGINT NOT NULL, + value VARCHAR(256) NOT NULL +); + +CREATE TABLE project ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title VARCHAR(1024) NOT NULL, + code VARCHAR(32) NOT NULL, + description VARCHAR(4096) NOT NULL, + parent_id BIGINT, + type_code VARCHAR(32) NOT NULL DEFAULT 'DEFAULT' +); + +CREATE TABLE sprint ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + code VARCHAR(32) NOT NULL, + project_id BIGINT, + status_code VARCHAR(32) NOT NULL +); + +CREATE TABLE task ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title VARCHAR(1024) NOT NULL, + parent_id BIGINT, + project_id BIGINT, + sprint_id BIGINT, + status_code VARCHAR(32) NOT NULL, + type_code VARCHAR(32) NOT NULL +); + +CREATE TABLE activity ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + author_id BIGINT NOT NULL, + task_id BIGINT NOT NULL, + updated TIMESTAMP, + comment VARCHAR(4096), + title VARCHAR(1024) NOT NULL, + description VARCHAR(4096), + estimate INTEGER, + type_code VARCHAR(32) NOT NULL, + status_code VARCHAR(32) NOT NULL, + priority_code VARCHAR(32) +); + +CREATE TABLE attachment ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(128) NOT NULL, + date_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + file_link VARCHAR(255) NOT NULL, + object_id BIGINT NOT NULL, + object_type SMALLINT NOT NULL, + user_id BIGINT NOT NULL +); + +CREATE TABLE task_tag ( + task_id BIGINT NOT NULL, + tag VARCHAR(255) +); + +CREATE TABLE user_belong ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + object_id BIGINT NOT NULL, + object_type SMALLINT NOT NULL, + user_id BIGINT NOT NULL, + user_type_code VARCHAR(32) NOT NULL +); + +CREATE TABLE user_role ( + user_id BIGINT NOT NULL, + role SMALLINT +); + +CREATE TABLE reference ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + endpoint TIMESTAMP, + startpoint TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title VARCHAR(1024) NOT NULL, + aux VARCHAR, + code VARCHAR(32) NOT NULL, + ref_type SMALLINT NOT NULL +); + +CREATE TABLE mail_case ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY, + email VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + date_time TIMESTAMP NOT NULL, + result VARCHAR(255) NOT NULL, + template VARCHAR(255) NOT NULL +); + +-- ========================= +-- PK / UNIQUE +-- ========================= +ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +ALTER TABLE profile ADD CONSTRAINT profile_pkey PRIMARY KEY (id); +ALTER TABLE contact ADD CONSTRAINT contact_pkey PRIMARY KEY (code, id); +ALTER TABLE project ADD CONSTRAINT project_pkey PRIMARY KEY (id); +ALTER TABLE sprint ADD CONSTRAINT sprint_pkey PRIMARY KEY (id); +ALTER TABLE task ADD CONSTRAINT task_pkey PRIMARY KEY (id); +ALTER TABLE activity ADD CONSTRAINT activity_pkey PRIMARY KEY (id); +ALTER TABLE attachment ADD CONSTRAINT attachment_pkey PRIMARY KEY (id); +ALTER TABLE user_belong ADD CONSTRAINT user_belong_pkey PRIMARY KEY (id); +ALTER TABLE reference ADD CONSTRAINT reference_pkey PRIMARY KEY (id); +ALTER TABLE mail_case ADD CONSTRAINT mail_case_pkey PRIMARY KEY (id); + +ALTER TABLE project ADD CONSTRAINT uk_project_code UNIQUE (code); +ALTER TABLE reference ADD CONSTRAINT uk_reference_ref_type_code UNIQUE (ref_type, code); +ALTER TABLE task_tag ADD CONSTRAINT uk_task_tag UNIQUE (task_id, tag); +ALTER TABLE user_belong ADD CONSTRAINT uk_user_belong UNIQUE (object_id, object_type, user_id, user_type_code); +ALTER TABLE user_role ADD CONSTRAINT uk_user_role UNIQUE (user_id, role); +ALTER TABLE users ADD CONSTRAINT uk_users_display_name UNIQUE (display_name); +ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email); + +-- ========================= +-- FKs +-- ========================= +ALTER TABLE profile + ADD CONSTRAINT fk_profile_users FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE contact + ADD CONSTRAINT fk_contact_profile FOREIGN KEY (id) REFERENCES profile(id) ON DELETE CASCADE; + +ALTER TABLE project + ADD CONSTRAINT fk_project_parent FOREIGN KEY (parent_id) REFERENCES project(id) ON DELETE CASCADE; + +ALTER TABLE sprint + ADD CONSTRAINT fk_sprint_project FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE; + +ALTER TABLE task + ADD CONSTRAINT fk_task_parent_task FOREIGN KEY (parent_id) REFERENCES task(id) ON DELETE CASCADE; +ALTER TABLE task + ADD CONSTRAINT fk_task_project FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE; +ALTER TABLE task + ADD CONSTRAINT fk_task_sprint FOREIGN KEY (sprint_id) REFERENCES sprint(id) ON DELETE SET NULL; + +ALTER TABLE activity + ADD CONSTRAINT fk_activity_users FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE activity + ADD CONSTRAINT fk_activity_task FOREIGN KEY (task_id) REFERENCES task(id) ON DELETE CASCADE; + +ALTER TABLE attachment + ADD CONSTRAINT fk_attachment FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +ALTER TABLE task_tag + ADD CONSTRAINT fk_task_tag FOREIGN KEY (task_id) REFERENCES task(id) ON DELETE CASCADE; + +ALTER TABLE user_belong + ADD CONSTRAINT fk_user_belong FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/src/test/resources/db/data-test.sql b/src/test/resources/db/data-test.sql new file mode 100644 index 000000000..9d6972e24 --- /dev/null +++ b/src/test/resources/db/data-test.sql @@ -0,0 +1,40 @@ +SET MODE PostgreSQL; +--SET REFERENTIAL_INTEGRITY FALSE; + +--TRUNCATE TABLE ACTIVITY; +--TRUNCATE TABLE ATTACHMENT; +--TRUNCATE TABLE CONTACT; +--TRUNCATE TABLE MAIL_CASE; +--TRUNCATE TABLE PROFILE; +--TRUNCATE TABLE PROJECT; +--TRUNCATE TABLE REFERENCE; +--TRUNCATE TABLE SPRINT; +--TRUNCATE TABLE TASK_TAG; +--TRUNCATE TABLE TASK; +--TRUNCATE TABLE USER_BELONG; +--TRUNCATE TABLE USER_ROLE; +--TRUNCATE TABLE USERS; + +SET REFERENTIAL_INTEGRITY TRUE; + +INSERT INTO USERS (ID, DISPLAY_NAME, EMAIL, FIRST_NAME, LAST_NAME, PASSWORD, STARTPOINT) +VALUES + (1, 'userDisplay', 'user@gmail.com', 'UserFirst', 'UserLast', '{noop}password', CURRENT_TIMESTAMP), + (2, 'adminDisplay', 'admin@gmail.com', 'AdminFirst', 'AdminLast', '{noop}admin', CURRENT_TIMESTAMP); + +INSERT INTO USER_ROLE (USER_ID, ROLE) VALUES (2, 1); + +INSERT INTO PROFILE (ID, LAST_LOGIN, LAST_FAILED_LOGIN, MAIL_NOTIFICATIONS) +VALUES + (1, NULL, NULL, FALSE), + (2, NULL, NULL, FALSE); + +INSERT INTO CONTACT (ID, CODE, VALUE) +VALUES + (1, 'EMAIL', 'user@gmail.com'), + (2, 'EMAIL', 'admin@gmail.com'); + +INSERT INTO REFERENCE (CODE, TITLE, REF_TYPE, STARTPOINT, AUX, ENDPOINT) VALUES + ('priority', 'Priority', 0, CURRENT_TIMESTAMP, NULL, NULL), + ('status', 'Status', 0, CURRENT_TIMESTAMP, NULL, NULL), + ('type', 'Task type',0, CURRENT_TIMESTAMP, NULL, NULL); diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 000000000..18d5d9d2a --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,59 @@ +SET MODE PostgreSQL; + +DROP TABLE IF EXISTS ACTIVITY; +DROP TABLE IF EXISTS ATTACHMENT; +DROP TABLE IF EXISTS CONTACT; +DROP TABLE IF EXISTS MAIL_CASE; +DROP TABLE IF EXISTS PROFILE; +DROP TABLE IF EXISTS PROJECT; +DROP TABLE IF EXISTS REFERENCE; +DROP TABLE IF EXISTS SPRINT; +DROP TABLE IF EXISTS TASK_TAG; +DROP TABLE IF EXISTS TASK; +DROP TABLE IF EXISTS USER_BELONG; +DROP TABLE IF EXISTS USER_ROLE; +DROP TABLE IF EXISTS USERS; + +CREATE TABLE USERS ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + EMAIL VARCHAR(255) NOT NULL UNIQUE, + PASSWORD VARCHAR(255) NOT NULL, + FIRST_NAME VARCHAR(255), + LAST_NAME VARCHAR(255), + DISPLAY_NAME VARCHAR(255) NOT NULL, + STARTPOINT TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +CREATE TABLE USER_ROLE ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + USER_ID BIGINT NOT NULL, + ROLE SMALLINT NOT NULL, + CONSTRAINT UK_USER_ROLE UNIQUE (USER_ID, ROLE) +); + +CREATE TABLE PROFILE ( + ID BIGINT PRIMARY KEY, + LAST_LOGIN TIMESTAMP, + LAST_FAILED_LOGIN TIMESTAMP, + MAIL_NOTIFICATIONS BOOLEAN DEFAULT FALSE, + CONSTRAINT FK_PROFILE_USER FOREIGN KEY (ID) REFERENCES USERS(ID) ON DELETE CASCADE +); + +CREATE TABLE CONTACT ( + ID BIGINT NOT NULL, + CODE VARCHAR(64) NOT NULL, + VALUE VARCHAR(1024), + PRIMARY KEY (ID, CODE), + CONSTRAINT FK_CONTACT_PROFILE FOREIGN KEY (ID) REFERENCES PROFILE(ID) ON DELETE CASCADE +); + +CREATE TABLE REFERENCE ( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + STARTPOINT TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + ENDPOINT TIMESTAMP, + TITLE VARCHAR(1024) NOT NULL, + AUX VARCHAR, + CODE VARCHAR(32) NOT NULL, + REF_TYPE SMALLINT NOT NULL +); +