Dev (#1702)
* Initial Typescript models. * Re-add realtime data * get distinct sessions for session metrics * Add queries for new schema. * Fix Typo. * Add some api/team endpoints. * Fix destructure error. * Fix getWebsites call. * Ignore typescript build errors. * Fix enum issue. * add clickhouse route to deleteWebsite * Fix Website auth. * Updated lint-staged config. * Add permission checks. * Add user role api. * Fix error when updating website. * Fix isAdmin check. Fix Schema. * Initial conversion to react-basics. * Remove user/team transfer from website update. * delete website in relational query * Fix login secure token creation. * Add event type to event. * Allow user to be added to team with role. * Updated login form. * Add Role to TeamUser. * Add database migration. * Refactored permissions check. Updated redis lib. * Feat/um 114 roles and permissions (#1683) * Auth checkpoint. * Merge branch 'dev' into feat/um-114-roles-and-permissions * Add 02 migration. * Added lib/types. * Updated schema. * Updated roles and permissions logic. * Implement react-basics styles. Fix queries. * Update website details layout. * Add 01 migration. * Fix admin create. * Update react-basics. Co-authored-by: Francis Cao <franciscao@gmail.com> Co-authored-by: Mike Cao <mike@mikecao.com> Co-authored-by: Mike Cao <moocao@gmail.com>
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M216.464 36.465l-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L387.887 239H12c-6.627 0-12 5.373-12 12v10c0 6.627 5.373 12 12 12h375.887L209.393 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L233.434 36.465c-4.686-4.687-12.284-4.687-16.97 0z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m216.464 36.465-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L387.887 239H12c-6.627 0-12 5.373-12 12v10c0 6.627 5.373 12 12 12h375.887L209.393 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L233.434 36.465c-4.686-4.687-12.284-4.687-16.97 0z"/></svg>
|
Before Width: | Height: | Size: 409 B After Width: | Height: | Size: 408 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M392 320C378.75 320 368 330.75 368 344V456C368 460.406 364.406 464 360 464H56C51.594 464 48 460.406 48 456V152C48 147.594 51.594 144 56 144H168C181.25 144 192 133.25 192 120S181.25 96 168 96H56C25.125 96 0 121.125 0 152V456C0 486.875 25.125 512 56 512H360C390.875 512 416 486.875 416 456V344C416 330.75 405.25 320 392 320ZM488 0H320C306.75 0 296 10.75 296 24S306.75 48 320 48H430.062L183.031 295.031C173.656 304.406 173.656 319.594 183.031 328.969C187.719 333.656 193.844 336 200 336S212.281 333.656 216.969 328.969L464 81.938V192C464 205.25 474.75 216 488 216S512 205.25 512 192V24C512 10.75 501.25 0 488 0Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M392 320c-13.25 0-24 10.75-24 24v112c0 4.406-3.594 8-8 8H56c-4.406 0-8-3.594-8-8V152c0-4.406 3.594-8 8-8h112c13.25 0 24-10.75 24-24s-10.75-24-24-24H56c-30.875 0-56 25.125-56 56v304c0 30.875 25.125 56 56 56h304c30.875 0 56-25.125 56-56V344c0-13.25-10.75-24-24-24ZM488 0H320c-13.25 0-24 10.75-24 24s10.75 24 24 24h110.062L183.031 295.031c-9.375 9.375-9.375 24.563 0 33.938A23.9 23.9 0 0 0 200 336a23.9 23.9 0 0 0 16.969-7.031L464 81.938V192c0 13.25 10.75 24 24 24s24-10.75 24-24V24c0-13.25-10.75-24-24-24Z"/></svg>
|
Before Width: | Height: | Size: 831 B After Width: | Height: | Size: 583 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M424 392H24C10.8 392 0 402.8 0 416V416C0 429.2 10.8 440 24 440H424C437.2 440 448 429.2 448 416V416C448 402.8 437.2 392 424 392ZM424 72H24C10.8 72 0 82.8 0 96V96C0 109.2 10.8 120 24 120H424C437.2 120 448 109.2 448 96V96C448 82.8 437.2 72 424 72ZM424 232H24C10.8 232 0 242.8 0 256V256C0 269.2 10.8 280 24 280H424C437.2 280 448 269.2 448 256V256C448 242.8 437.2 232 424 232Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M424 392H24c-13.2 0-24 10.8-24 24s10.8 24 24 24h400c13.2 0 24-10.8 24-24s-10.8-24-24-24Zm0-320H24C10.8 72 0 82.8 0 96s10.8 24 24 24h400c13.2 0 24-10.8 24-24s-10.8-24-24-24Zm0 160H24c-13.2 0-24 10.8-24 24s10.8 24 24 24h400c13.2 0 24-10.8 24-24s-10.8-24-24-24Z"/></svg>
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 338 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 748 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M435.848 83.466 172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 360 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M441.9 167.3l-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m441.9 167.3-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 271 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M234.8 511.7L196 500.4c-4.2-1.2-6.7-5.7-5.5-9.9L331.3 5.8c1.2-4.2 5.7-6.7 9.9-5.5L380 11.6c4.2 1.2 6.7 5.7 5.5 9.9L244.7 506.2c-1.2 4.3-5.6 6.7-9.9 5.5zm-83.2-121.1l27.2-29c3.1-3.3 2.8-8.5-.5-11.5L72.2 256l106.1-94.1c3.4-3 3.6-8.2.5-11.5l-27.2-29c-3-3.2-8.1-3.4-11.3-.4L2.5 250.2c-3.4 3.2-3.4 8.5 0 11.7L140.3 391c3.2 3 8.2 2.8 11.3-.4zm284.1.4l137.7-129.1c3.4-3.2 3.4-8.5 0-11.7L435.7 121c-3.2-3-8.3-2.9-11.3.4l-27.2 29c-3.1 3.3-2.8 8.5.5 11.5L503.8 256l-106.1 94.1c-3.4 3-3.6 8.2-.5 11.5l27.2 29c3.1 3.2 8.1 3.4 11.3.4z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M234.8 511.7 196 500.4c-4.2-1.2-6.7-5.7-5.5-9.9L331.3 5.8c1.2-4.2 5.7-6.7 9.9-5.5L380 11.6c4.2 1.2 6.7 5.7 5.5 9.9L244.7 506.2c-1.2 4.3-5.6 6.7-9.9 5.5zm-83.2-121.1 27.2-29c3.1-3.3 2.8-8.5-.5-11.5L72.2 256l106.1-94.1c3.4-3 3.6-8.2.5-11.5l-27.2-29c-3-3.2-8.1-3.4-11.3-.4L2.5 250.2c-3.4 3.2-3.4 8.5 0 11.7L140.3 391c3.2 3 8.2 2.8 11.3-.4zm284.1.4 137.7-129.1c3.4-3.2 3.4-8.5 0-11.7L435.7 121c-3.2-3-8.3-2.9-11.3.4l-27.2 29c-3.1 3.3-2.8 8.5.5 11.5L503.8 256l-106.1 94.1c-3.4 3-3.6 8.2-.5 11.5l27.2 29c3.1 3.2 8.1 3.4 11.3.4z"/></svg>
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M497.6,0,334.4.17A14.4,14.4,0,0,0,320,14.57V47.88a14.4,14.4,0,0,0,14.69,14.4l73.63-2.72,2.06,2.06L131.52,340.49a12,12,0,0,0,0,17l23,23a12,12,0,0,0,17,0L450.38,101.62l2.06,2.06-2.72,73.63A14.4,14.4,0,0,0,464.12,192h33.31a14.4,14.4,0,0,0,14.4-14.4L512,14.4A14.4,14.4,0,0,0,497.6,0ZM432,288H416a16,16,0,0,0-16,16V458a6,6,0,0,1-6,6H54a6,6,0,0,1-6-6V118a6,6,0,0,1,6-6H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V304A16,16,0,0,0,432,288Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M497.6 0 334.4.17a14.4 14.4 0 0 0-14.4 14.4v33.31a14.4 14.4 0 0 0 14.69 14.4l73.63-2.72 2.06 2.06-278.86 278.87a12 12 0 0 0 0 17l23 23a12 12 0 0 0 17 0l278.86-278.87 2.06 2.06-2.72 73.63a14.4 14.4 0 0 0 14.4 14.69h33.31a14.4 14.4 0 0 0 14.4-14.4L512 14.4A14.4 14.4 0 0 0 497.6 0ZM432 288h-16a16 16 0 0 0-16 16v154a6 6 0 0 1-6 6H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h154a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V304a16 16 0 0 0-16-16Z"/></svg>
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 573 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M504.265 315.978C504.265 307.326 499.658 299.134 491.906 294.586L458.998 275.615C459.643 269.099 459.966 262.549 459.966 256S459.643 242.901 458.998 236.385L491.906 217.414C499.658 212.866 504.265 204.674 504.265 196.022C504.265 174.755 454.947 67.846 419.746 67.846C415.502 67.846 411.236 68.939 407.379 71.203L374.599 90.172C363.888 82.43 352.533 75.848 340.531 70.428V32.488C340.531 21.262 333.047 11.453 322.205 8.613C300.654 2.871 278.425 0 256.181 0C233.935 0 211.675 2.871 190.06 8.613C179.218 11.453 171.734 21.262 171.734 32.488V70.428C159.732 75.848 148.377 82.43 137.666 90.172L104.886 71.203C101.031 68.939 96.763 67.846 92.519 67.846C92.517 67.846 92.514 67.846 92.512 67.846C60.048 67.846 8 169.591 8 196.022C8 204.674 12.607 212.866 20.359 217.414L53.267 236.385C52.622 242.901 52.299 249.451 52.299 256S52.622 269.099 53.267 275.615L20.359 294.586C12.607 299.134 8 307.326 8 315.978C8 337.245 57.318 444.154 92.519 444.154C96.763 444.154 101.029 443.061 104.886 440.797L137.666 421.828C148.377 429.57 159.732 436.152 171.734 441.572V479.512C171.734 490.738 179.218 500.547 190.06 503.387C211.611 509.129 233.84 512 256.084 512C278.33 512 300.59 509.129 322.205 503.387C333.047 500.547 340.531 490.738 340.531 479.512V441.572C352.533 436.152 363.888 429.57 374.599 421.828L407.379 440.797C411.234 443.061 415.502 444.154 419.746 444.154C452.209 444.154 504.265 342.423 504.265 315.978ZM415.361 389.959C391.561 376.186 404.101 383.444 371.705 364.695C329.649 395.09 339.375 389.426 292.531 410.582V460.82C279.236 463.161 266.948 464 256.093 464C240.669 464 228.14 462.306 219.734 460.824V410.582C172.779 389.376 182.552 395.044 140.56 364.695C108.748 383.105 117.896 377.811 96.924 389.949C81.181 371.256 68.849 349.895 60.517 326.84C81.643 314.663 72.361 320.014 104.088 301.723C101.549 276.083 100.277 266.079 100.277 256.04C100.277 246.018 101.545 235.96 104.088 210.277C72.198 191.892 81.571 197.295 60.504 185.152C68.818 162.109 81.187 140.686 96.904 122.041C120.704 135.814 108.164 128.556 140.56 147.305C182.616 116.91 172.89 122.574 219.734 101.418V51.18C233.029 48.839 245.318 48 256.172 48C271.597 48 284.126 49.694 292.531 51.176V101.418C339.486 122.624 329.713 116.956 371.705 147.305C405.655 127.657 394.228 134.27 415.343 122.051C431.084 140.744 443.416 162.105 451.748 185.16C430.622 197.337 439.904 191.986 408.177 210.277C410.716 235.917 411.988 245.921 411.988 255.96C411.988 265.982 410.72 276.04 408.177 301.723C440.067 320.108 430.694 314.705 451.761 326.848C443.447 349.891 431.078 371.314 415.361 389.959ZM256.133 160C203.258 160 160.133 203.125 160.133 256S203.258 352 256.133 352S352.133 308.875 352.133 256S309.008 160 256.133 160ZM256.133 304C229.666 304 208.133 282.467 208.133 256S229.666 208 256.133 208S304.133 229.533 304.133 256S282.599 304 256.133 304Z "></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M504.265 315.978c0-8.652-4.607-16.844-12.359-21.392l-32.908-18.971a199.182 199.182 0 0 0 0-39.23l32.908-18.971c7.752-4.548 12.359-12.74 12.359-21.392 0-21.267-49.318-128.176-84.519-128.176-4.244 0-8.51 1.093-12.367 3.357l-32.78 18.969a195.058 195.058 0 0 0-34.068-19.744v-37.94c0-11.226-7.484-21.035-18.326-23.875C300.654 2.871 278.425 0 256.181 0a257.698 257.698 0 0 0-66.121 8.613c-10.842 2.84-18.326 12.649-18.326 23.875v37.94a195.058 195.058 0 0 0-34.068 19.744l-32.78-18.969a24.36 24.36 0 0 0-12.367-3.357h-.007C60.048 67.846 8 169.591 8 196.022c0 8.652 4.607 16.844 12.359 21.392l32.908 18.971a199.182 199.182 0 0 0 0 39.23l-32.908 18.971C12.607 299.134 8 307.326 8 315.978c0 21.267 49.318 128.176 84.519 128.176 4.244 0 8.51-1.093 12.367-3.357l32.78-18.969a195.058 195.058 0 0 0 34.068 19.744v37.94c0 11.226 7.484 21.035 18.326 23.875 21.551 5.742 43.78 8.613 66.024 8.613 22.246 0 44.506-2.871 66.121-8.613 10.842-2.84 18.326-12.649 18.326-23.875v-37.94a195.058 195.058 0 0 0 34.068-19.744l32.78 18.969a24.36 24.36 0 0 0 12.367 3.357c32.463 0 84.519-101.731 84.519-128.176Zm-88.904 73.981c-23.8-13.773-11.26-6.515-43.656-25.264-42.056 30.395-32.33 24.731-79.174 45.887v50.238a210.138 210.138 0 0 1-36.438 3.18 208.924 208.924 0 0 1-36.359-3.176v-50.242c-46.955-21.206-37.182-15.538-79.174-45.887l-43.636 25.254a207.379 207.379 0 0 1-36.407-63.109c21.126-12.177 11.844-6.826 43.571-25.117-2.539-25.64-3.811-35.644-3.811-45.683 0-10.022 1.268-20.08 3.811-45.763-31.89-18.385-22.517-12.982-43.584-25.125a207.107 207.107 0 0 1 36.4-63.111c23.8 13.773 11.26 6.515 43.656 25.264 42.056-30.395 32.33-24.731 79.174-45.887V51.18A210.146 210.146 0 0 1 256.172 48c15.425 0 27.954 1.694 36.359 3.176v50.242c46.955 21.206 37.182 15.538 79.174 45.887l43.638-25.254a207.414 207.414 0 0 1 36.405 63.109c-21.126 12.177-11.844 6.826-43.571 25.117 2.539 25.64 3.811 35.644 3.811 45.683 0 10.022-1.268 20.08-3.811 45.763 31.89 18.385 22.517 12.982 43.584 25.125a207.107 207.107 0 0 1-36.4 63.111ZM256.133 160c-52.875 0-96 43.125-96 96s43.125 96 96 96 96-43.125 96-96-43.125-96-96-96Zm0 144c-26.467 0-48-21.533-48-48s21.533-48 48-48 48 21.533 48 48-21.534 48-48 48Z"/></svg>
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.2 KiB |
@ -1,2 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428 389.11" width="20" height="20">
|
||||
<circle cx="214.15" cy="181" r="171" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="20"/><path d="M413,134.11H15.29a15,15,0,0,0-15,15v15.3C.12,168,0,171.52,0,175.11c0,118.19,95.81,214,214,214,116.4,0,211.1-92.94,213.93-208.67,0-.44.07-.88.07-1.33v-30A15,15,0,0,0,413,134.11Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428 389.11" width="20" height="20"><circle cx="214.15" cy="181" r="171" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="20"/><path d="M413 134.11H15.29a15 15 0 0 0-15 15v15.3C.12 168 0 171.52 0 175.11c0 118.19 95.81 214 214 214 116.4 0 211.1-92.94 213.93-208.67 0-.44.07-.88.07-1.33v-30a15 15 0 0 0-15-15Z"/></svg>
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 390 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1399.98 1400"><path d="M562.44,837.55C335.89,611,288.08,273.54,418.71,0A734.31,734.31,0,0,0,215.54,143.73c-287.39,287.39-287.39,753.33,0,1040.72s753.33,287.4,1040.74,0A733.8,733.8,0,0,0,1400,981.29C1126.45,1111.92,789,1064.09,562.44,837.55Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1399.98 1400"><path d="M562.44 837.55C335.89 611 288.08 273.54 418.71 0a734.31 734.31 0 0 0-203.17 143.73c-287.39 287.39-287.39 753.33 0 1040.72s753.33 287.4 1040.74 0A733.8 733.8 0 0 0 1400 981.29c-273.55 130.63-611 82.8-837.56-143.74Z"/></svg>
|
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 298 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M493.26 56.26l-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57l-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m493.26 56.26-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z"/></svg>
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 578 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1400"><path d="M367.43,422.13a54.44,54.44,0,0,1-38.66-16L205,282.35A54.69,54.69,0,0,1,282.37,205L406.11,328.79a54.68,54.68,0,0,1-38.68,93.34Z"/><path d="M1156.3,1211a54.51,54.51,0,0,1-38.67-16L993.89,1071.21a54.68,54.68,0,1,1,77.34-77.33L1195,1117.65A54.7,54.7,0,0,1,1156.3,1211Z"/><path d="M243.7,1211A54.7,54.7,0,0,1,205,1117.65L328.74,993.89a54.69,54.69,0,0,1,77.36,77.32L282.37,1195A54.51,54.51,0,0,1,243.7,1211Z"/><path d="M1032.57,422.13a54.68,54.68,0,0,1-38.68-93.34L1117.61,205A54.69,54.69,0,0,1,1195,282.35L1071.23,406.11A54.44,54.44,0,0,1,1032.57,422.13Z"/><path d="M229.69,754.69h-175a54.69,54.69,0,0,1,0-109.38h175a54.69,54.69,0,0,1,0,109.38Z"/><path d="M1345.31,754.69h-175a54.69,54.69,0,0,1,0-109.38h175a54.69,54.69,0,0,1,0,109.38Z"/><path d="M700,1400a54.68,54.68,0,0,1-54.69-54.69v-175a54.69,54.69,0,0,1,109.38,0v175A54.68,54.68,0,0,1,700,1400Z"/><path d="M700,284.38a54.7,54.7,0,0,1-54.69-54.69v-175a54.69,54.69,0,0,1,109.38,0v175A54.7,54.7,0,0,1,700,284.38Z"/><circle cx="700" cy="700" r="306.25"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1400"><path d="M367.43 422.13a54.44 54.44 0 0 1-38.66-16L205 282.35A54.69 54.69 0 0 1 282.37 205l123.74 123.79a54.68 54.68 0 0 1-38.68 93.34ZM1156.3 1211a54.51 54.51 0 0 1-38.67-16l-123.74-123.79a54.68 54.68 0 1 1 77.34-77.33L1195 1117.65a54.7 54.7 0 0 1-38.7 93.35ZM243.7 1211a54.7 54.7 0 0 1-38.7-93.35l123.74-123.76a54.69 54.69 0 0 1 77.36 77.32L282.37 1195a54.51 54.51 0 0 1-38.67 16ZM1032.57 422.13a54.68 54.68 0 0 1-38.68-93.34L1117.61 205a54.69 54.69 0 0 1 77.39 77.35l-123.77 123.76a54.44 54.44 0 0 1-38.66 16.02ZM229.69 754.69h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38ZM1345.31 754.69h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38ZM700 1400a54.68 54.68 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.68 54.68 0 0 1 700 1400ZM700 284.38a54.7 54.7 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.7 54.7 0 0 1 700 284.38Z"/><circle cx="700" cy="700" r="306.25"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 989 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="m207.6 256 107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 468 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1652 1652"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M1587.07,504.47A828.56,828.56,0,1,0,1652,826,823.13,823.13,0,0,0,1587.07,504.47ZM826,1577a747.29,747.29,0,0,1-464.48-161.26,39.94,39.94,0,0,0,2.8-11.35,458.82,458.82,0,0,1,34.29-135.74,464.15,464.15,0,0,1,854.78,0,458.82,458.82,0,0,1,34.29,135.74,39.94,39.94,0,0,0,2.8,11.35A747.29,747.29,0,0,1,826,1577ZM719.81,866.57A274,274,0,1,1,826,888,272.1,272.1,0,0,1,719.81,866.57Zm641.28,485.87c-36.11-201.1-182.78-363.82-374.86-423,114.28-58.37,192.53-177.22,192.53-314.35,0-194.83-157.94-352.76-352.76-352.76S473.24,420.29,473.24,615.12c0,137.13,78.25,256,192.53,314.35-192.08,59.15-338.75,221.87-374.86,423C157.46,1216.81,75,1030.86,75,826,75,411.9,411.9,75,826,75s751,336.9,751,751C1577,1030.86,1494.54,1216.81,1361.09,1352.44Z"/></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1652 1652"><g data-name="Layer 2"><path d="M1587.07 504.47A828.56 828.56 0 1 0 1652 826a823.13 823.13 0 0 0-64.93-321.53ZM826 1577a747.29 747.29 0 0 1-464.48-161.26 39.94 39.94 0 0 0 2.8-11.35 458.82 458.82 0 0 1 34.29-135.74 464.15 464.15 0 0 1 854.78 0 458.82 458.82 0 0 1 34.29 135.74 39.94 39.94 0 0 0 2.8 11.35A747.29 747.29 0 0 1 826 1577ZM719.81 866.57A274 274 0 1 1 826 888a272.1 272.1 0 0 1-106.19-21.43Zm641.28 485.87c-36.11-201.1-182.78-363.82-374.86-423 114.28-58.37 192.53-177.22 192.53-314.35 0-194.83-157.94-352.76-352.76-352.76S473.24 420.29 473.24 615.12c0 137.13 78.25 256 192.53 314.35-192.08 59.15-338.75 221.87-374.86 423C157.46 1216.81 75 1030.86 75 826 75 411.9 411.9 75 826 75s751 336.9 751 751c0 204.86-82.46 390.81-215.91 526.44Z" data-name="Layer 1"/></g></svg>
|
Before Width: | Height: | Size: 910 B After Width: | Height: | Size: 841 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M312.973 375.032C322.342 384.401 322.342 399.604 312.973 408.973S288.401 418.342 279.032 408.973L160 289.941L40.968 408.973C31.599 418.342 16.396 418.342 7.027 408.973S-2.342 384.401 7.027 375.032L126.059 256L7.027 136.968C-2.342 127.599 -2.342 112.396 7.027 103.027S31.599 93.658 40.968 103.027L160 222.059L279.032 103.027C288.401 93.658 303.604 93.658 312.973 103.027S322.342 127.599 312.973 136.968L193.941 256L312.973 375.032Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M312.973 375.032c9.369 9.369 9.369 24.572 0 33.941s-24.572 9.369-33.941 0L160 289.941 40.968 408.973c-9.369 9.369-24.572 9.369-33.941 0s-9.369-24.572 0-33.941L126.059 256 7.027 136.968c-9.369-9.369-9.369-24.572 0-33.941s24.572-9.369 33.941 0L160 222.059l119.032-119.032c9.369-9.369 24.572-9.369 33.941 0s9.369 24.572 0 33.941L193.941 256l119.032 119.032Z"/></svg>
|
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 434 B |
@ -2,9 +2,9 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
color: var(--gray900);
|
||||
background: var(--gray100);
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--base900);
|
||||
background: var(--base100);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
@ -14,11 +14,11 @@
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--gray200);
|
||||
background: var(--base200);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -29,30 +29,30 @@
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.action,
|
||||
.action:active {
|
||||
color: var(--gray50);
|
||||
background: var(--gray900);
|
||||
color: var(--base50);
|
||||
background: var(--base900);
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: var(--gray800);
|
||||
background: var(--base800);
|
||||
}
|
||||
|
||||
.danger,
|
||||
.danger:active {
|
||||
color: var(--gray50);
|
||||
color: var(--base50);
|
||||
background: var(--red500);
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
.light,
|
||||
.light:active {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@ -85,18 +85,18 @@
|
||||
|
||||
.button:disabled {
|
||||
cursor: default;
|
||||
color: var(--gray500);
|
||||
background: var(--gray75);
|
||||
color: var(--base500);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.button:disabled:active {
|
||||
color: var(--gray500);
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.button:disabled:hover {
|
||||
background: var(--gray75);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.button.light:disabled {
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
}
|
||||
|
@ -2,14 +2,14 @@
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray500);
|
||||
border: 1px solid var(--base500);
|
||||
}
|
||||
|
||||
.group .button {
|
||||
border-radius: 0;
|
||||
color: var(--gray800);
|
||||
background: var(--gray50);
|
||||
border-left: 1px solid var(--gray500);
|
||||
color: var(--base800);
|
||||
background: var(--base50);
|
||||
border-left: 1px solid var(--base500);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.group .button:hover {
|
||||
background: var(--gray100);
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.group .button + .button {
|
||||
@ -26,6 +26,6 @@
|
||||
}
|
||||
|
||||
.group .button.selected {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
flex: 1;
|
||||
min-height: 306px;
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
color: var(--gray800);
|
||||
color: var(--base800);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
@ -23,17 +23,17 @@
|
||||
}
|
||||
|
||||
.calendar td:hover {
|
||||
border: 1px solid var(--gray300);
|
||||
background: var(--gray75);
|
||||
border: 1px solid var(--base300);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.calendar td.faded {
|
||||
color: var(--gray500);
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.calendar td.selected {
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--gray600);
|
||||
border: 1px solid var(--base600);
|
||||
}
|
||||
|
||||
.calendar td.selected:hover {
|
||||
@ -41,18 +41,18 @@
|
||||
}
|
||||
|
||||
.calendar td.disabled {
|
||||
color: var(--gray400);
|
||||
background: var(--gray75);
|
||||
color: var(--base400);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.calendar td.disabled:hover {
|
||||
cursor: default;
|
||||
background: var(--gray75);
|
||||
background: var(--base75);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.calendar td.faded.disabled {
|
||||
background: var(--gray100);
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -61,7 +61,7 @@
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.body {
|
||||
|
@ -11,7 +11,7 @@
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--gray500);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
.wrapper {
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--gray500);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
padding: 4px 16px;
|
||||
|
@ -6,7 +6,7 @@
|
||||
margin: auto;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
background-color: var(--gray50);
|
||||
background-color: var(--base50);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,11 @@
|
||||
}
|
||||
|
||||
.row .inactive {
|
||||
color: var(--gray500);
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.row .active {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ a.link,
|
||||
a.link:active,
|
||||
a.link:visited {
|
||||
position: relative;
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -17,15 +17,15 @@ a.link:hover span {
|
||||
}
|
||||
|
||||
a.link.large {
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
a.link.small {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
a.link.xsmall {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
a.link .icon + * {
|
||||
|
@ -25,7 +25,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
background: var(--gray400);
|
||||
background: var(--base400);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@ -33,13 +33,13 @@
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
background: var(--gray400);
|
||||
background: var(--base400);
|
||||
animation: blink 1.4s infinite;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.loading.overlay div {
|
||||
background: var(--gray900);
|
||||
background: var(--base900);
|
||||
}
|
||||
|
||||
.loading div + div {
|
||||
|
@ -1,22 +1,22 @@
|
||||
.menu {
|
||||
background: var(--gray50);
|
||||
border: 1px solid var(--gray500);
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.option {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: normal;
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
padding: 4px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--gray100);
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.float {
|
||||
@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
|
@ -10,11 +10,11 @@
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.open,
|
||||
.open:hover {
|
||||
background: var(--gray50);
|
||||
border: 1px solid var(--gray500);
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--gray50);
|
||||
background-color: var(--base50);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
|
@ -25,12 +25,12 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
min-width: 400px;
|
||||
min-height: 100px;
|
||||
max-width: 100vw;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--gray300);
|
||||
border: 1px solid var(--base300);
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.menu {
|
||||
color: var(--gray800);
|
||||
border: 1px solid var(--gray500);
|
||||
color: var(--base800);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
@ -13,10 +13,10 @@
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--gray75);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
color: var(--gray500);
|
||||
font-size: var(--font-size-normal);
|
||||
color: var(--base500);
|
||||
font-size: var(--font-size-md);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -5,16 +5,16 @@
|
||||
|
||||
.table label {
|
||||
display: none;
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.head {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
}
|
||||
@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
.row {
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
.tag {
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--gray300);
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.close {
|
||||
|
@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
2
components/declarations.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare module '*.css';
|
||||
declare module '*.svg';
|
@ -16,7 +16,7 @@
|
||||
.calendars > div + div {
|
||||
margin-left: 20px;
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid var(--gray300);
|
||||
border-left: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.filter {
|
||||
|
@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.form {
|
||||
border-right: 1px solid var(--gray300);
|
||||
border-right: 1px solid var(--base300);
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.filters + .filters {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
|
63
components/forms/Form.module.css
Normal file
@ -0,0 +1,63 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
width: 600px;
|
||||
margin: 0 auto 30px;
|
||||
background: var(--base50);
|
||||
padding: 16px;
|
||||
color: var(--red400);
|
||||
border: 1px solid var(--red400);
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
width: 600px;
|
||||
margin: 60px auto;
|
||||
background: var(--base50);
|
||||
padding: 16px;
|
||||
color: var(--green400);
|
||||
border: 1px solid var(--green400);
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
@ -1,113 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { setItem } from 'next-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import {
|
||||
Form,
|
||||
FormInput,
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import Icon from 'components/common/Icon';
|
||||
import useApi from 'hooks/useApi';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
TextField,
|
||||
PasswordField,
|
||||
SubmitButton,
|
||||
Icon,
|
||||
} from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useApi } from 'next-basics';
|
||||
import { setUser } from 'store/app';
|
||||
import { setAuthToken } from 'lib/client';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './LoginForm.module.css';
|
||||
|
||||
const validate = ({ username, password }) => {
|
||||
const errors = {};
|
||||
|
||||
if (!username) {
|
||||
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!password) {
|
||||
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
import styles from './Form.module.css';
|
||||
|
||||
export default function LoginForm() {
|
||||
const { post } = useApi();
|
||||
const router = useRouter();
|
||||
const [message, setMessage] = useState();
|
||||
const { post } = useApi();
|
||||
const { mutate, error, isLoading } = useMutation(data => post('/auth/login', data));
|
||||
|
||||
const handleSubmit = async ({ username, password }) => {
|
||||
const { ok, status, data } = await post('/auth/login', {
|
||||
username,
|
||||
password,
|
||||
const handleSubmit = async data => {
|
||||
mutate(data, {
|
||||
onSuccess: async ({ token, user }) => {
|
||||
setAuthToken(token);
|
||||
setUser(user);
|
||||
|
||||
await router.push('/websites');
|
||||
},
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
const { user, token } = data;
|
||||
|
||||
setItem(AUTH_TOKEN, token);
|
||||
|
||||
setUser(user);
|
||||
|
||||
await router.push('/');
|
||||
|
||||
return null;
|
||||
} else {
|
||||
setMessage(
|
||||
status === 401 ? (
|
||||
<FormattedMessage
|
||||
id="message.incorrect-username-password"
|
||||
defaultMessage="Incorrect username/password."
|
||||
/>
|
||||
) : (
|
||||
data
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout className={styles.login}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
username: '',
|
||||
password: '',
|
||||
}}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<div className={styles.header}>
|
||||
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
|
||||
<h1 className="center">umami</h1>
|
||||
</div>
|
||||
<FormRow>
|
||||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="label.login" defaultMessage="Login" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<Icon size="xl">
|
||||
<Logo />
|
||||
</Icon>
|
||||
<p>umami</p>
|
||||
</div>
|
||||
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
|
||||
<FormInput name="username" label="Username" rules={{ required: 'Required' }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
<FormInput name="password" label="Password" rules={{ required: 'Required' }}>
|
||||
<PasswordField />
|
||||
</FormInput>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" className={styles.button} disabled={isLoading}>
|
||||
Log in
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@
|
||||
.msg {
|
||||
color: var(--msgColor);
|
||||
background: var(--red400);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
@ -78,8 +78,8 @@
|
||||
margin: 20px 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--gray50);
|
||||
background: var(--gray800);
|
||||
color: var(--base50);
|
||||
background: var(--base800);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
|
@ -9,12 +9,12 @@
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--gray300);
|
||||
border-left: 1px solid var(--base300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
border-left: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
18
components/layout/GridRow.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { Row, cloneChildren } from 'react-basics';
|
||||
import styles from './GridRow.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function GridRow(props) {
|
||||
const { children, className, ...rowProps } = props;
|
||||
return (
|
||||
<Row {...rowProps} className={className}>
|
||||
{breakpoint =>
|
||||
cloneChildren(children, () => {
|
||||
return {
|
||||
className: classNames(styles.column, styles[breakpoint]),
|
||||
};
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
);
|
||||
}
|
21
components/layout/GridRow.module.css
Normal file
@ -0,0 +1,21 @@
|
||||
.column {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--base200);
|
||||
border-left: 1px solid var(--base200);
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.column:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.column.xs,
|
||||
.column.sm,
|
||||
.column.md {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/common/Link';
|
||||
import Icon from 'components/common/Icon';
|
||||
import LanguageButton from 'components/settings/LanguageButton';
|
||||
@ -25,31 +25,33 @@ export default function Header() {
|
||||
return (
|
||||
<>
|
||||
{allowUpdate && <UpdateNotice />}
|
||||
<header className={classNames(styles.header, 'row')}>
|
||||
<div className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
|
||||
</div>
|
||||
<HamburgerButton />
|
||||
{user && !adminDisabled && (
|
||||
<div className={styles.links}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
<SettingsButton />
|
||||
{user && !adminDisabled && <UserButton />}
|
||||
</div>
|
||||
<header className={styles.header}>
|
||||
<Row>
|
||||
<Column className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
|
||||
</Column>
|
||||
<HamburgerButton />
|
||||
{user && !adminDisabled && (
|
||||
<div className={styles.links}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Column className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
<SettingsButton />
|
||||
{user && !adminDisabled && <UserButton />}
|
||||
</Column>
|
||||
</Row>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
|
@ -7,10 +7,10 @@
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@ -22,7 +22,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
.buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Container } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import Header from 'components/layout/Header';
|
||||
import Footer from 'components/layout/Footer';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function Layout({ title, children, header = true, footer = true }) {
|
||||
const { dir } = useLocale();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container dir={dir} style={{ maxWidth: 1140 }}>
|
||||
<Head>
|
||||
<title>umami{title && ` - ${title}`}</title>
|
||||
<title>{title ? `${title} | umami` : 'umami'}</title>
|
||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
|
||||
{header && <Header />}
|
||||
<main>{children}</main>
|
||||
{footer && <Footer />}
|
||||
<div id="__modals" dir={dir} />
|
||||
</>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
.container .content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-left: 1px solid var(--gray300);
|
||||
border-left: 1px solid var(--base300);
|
||||
padding-left: 30px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.container .content {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
|
@ -3,5 +3,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 30px;
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.value {
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
margin-right: 8px;
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.color {
|
||||
|
@ -1,7 +1,7 @@
|
||||
.table {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
display: grid;
|
||||
grid-template-rows: fit-content(100%) auto;
|
||||
overflow: hidden;
|
||||
@ -23,11 +23,11 @@
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
@ -80,8 +80,8 @@
|
||||
.percent {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
color: var(--gray600);
|
||||
border-left: 1px solid var(--gray600);
|
||||
color: var(--base600);
|
||||
border-left: 1px solid var(--base600);
|
||||
padding-left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -17,5 +17,5 @@
|
||||
}
|
||||
|
||||
.hidden {
|
||||
color: var(--gray400);
|
||||
color: var(--base400);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: var(--font-size-xlarge);
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: 40px;
|
||||
min-height: 40px;
|
||||
font-weight: 600;
|
||||
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-normal);
|
||||
font-size: var(--font-size-md);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -26,8 +26,8 @@
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
margin-left: 4px;
|
||||
border: 1px solid var(--gray200);
|
||||
color: var(--gray500);
|
||||
border: 1px solid var(--base200);
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.change.positive {
|
||||
|
@ -2,7 +2,7 @@
|
||||
position: relative;
|
||||
min-height: 430px;
|
||||
height: 100%;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.table {
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-size: var(--font-size-xs);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
@ -19,7 +19,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.body {
|
||||
@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
.row .link {
|
||||
color: var(--gray900);
|
||||
color: var(--base900);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
@ -20,7 +20,6 @@ export default function WebsiteChart({
|
||||
title,
|
||||
domain,
|
||||
stickyHeader = false,
|
||||
showLink = false,
|
||||
showChart = true,
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
@ -80,33 +79,34 @@ export default function WebsiteChart({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} showLink={showLink} />
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
<StickyHeader
|
||||
className={classNames(styles.metrics, 'col row')}
|
||||
stickyClassName={styles.sticky}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
<FilterTags
|
||||
params={{ url, referrer, os, browser, device, country }}
|
||||
onClick={handleCloseFilter}
|
||||
/>
|
||||
<div className="col-12 col-lg-9">
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} title={title} domain={domain} />
|
||||
|
||||
<StickyHeader
|
||||
className={styles.metrics}
|
||||
stickyClassName={styles.sticky}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
<FilterTags
|
||||
params={{ url, referrer, os, browser, device, country }}
|
||||
onClick={handleCloseFilter}
|
||||
/>
|
||||
<Row className={styles.header}>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={10}>
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</div>
|
||||
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
|
||||
</Column>
|
||||
<Column className={styles.filter} xs={12} sm={12} md={12} defaultSize={2}>
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
</div>
|
||||
</StickyHeader>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className={classNames(styles.chart, 'col')}>
|
||||
</Column>
|
||||
</Row>
|
||||
</StickyHeader>
|
||||
<Row>
|
||||
<Column className={styles.chart}>
|
||||
{error && <ErrorMessage />}
|
||||
{showChart && (
|
||||
<PageviewsChart
|
||||
@ -117,8 +117,8 @@ export default function WebsiteChart({
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,11 @@
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 60px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@ -31,19 +32,11 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
background: var(--gray50);
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
background: var(--base50);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.filter {
|
||||
display: block;
|
||||
}
|
||||
align-self: center;
|
||||
}
|
||||
|
@ -1,57 +1,22 @@
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import classNames from 'classnames';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import Link from 'components/common/Link';
|
||||
import OverflowText from 'components/common/OverflowText';
|
||||
import RefreshButton from 'components/common/RefreshButton';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ActiveUsers from './ActiveUsers';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||
const header = showLink ? (
|
||||
<>
|
||||
<Favicon domain={domain} />
|
||||
<Link
|
||||
className={styles.titleLink}
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteId}/${title}`}
|
||||
>
|
||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Favicon domain={domain} />
|
||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||
</>
|
||||
);
|
||||
|
||||
export default function WebsiteHeader({ websiteId, title, domain }) {
|
||||
return (
|
||||
<PageHeader className="row">
|
||||
<div className={classNames(styles.title, 'col-10 col-lg-4 order-1 order-lg-1')}>{header}</div>
|
||||
<div className={classNames(styles.active, 'col-12 col-lg-4 order-3 order-lg-2')}>
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
</div>
|
||||
<div className="col-2 col-lg-4 order-2 order-lg-3">
|
||||
<ButtonLayout align="right">
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
{showLink && (
|
||||
<Link
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteId}/${title}`}
|
||||
className={styles.link}
|
||||
icon={<Arrow />}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
</Link>
|
||||
)}
|
||||
</ButtonLayout>
|
||||
</div>
|
||||
<PageHeader>
|
||||
<Row>
|
||||
<Column className={styles.title} variant="two">
|
||||
<Favicon domain={domain} />
|
||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||
</Column>
|
||||
<Column className={styles.active} variant="two">
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
</Column>
|
||||
</Row>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
@ -1,32 +1,15 @@
|
||||
.title {
|
||||
color: var(--gray900);
|
||||
font-size: var(--font-size-large);
|
||||
line-height: var(--font-size-xlarge);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: var(--font-size-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titleLink {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.active {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
a.link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -16,19 +16,19 @@
|
||||
|
||||
.item h2 {
|
||||
font-size: 14px;
|
||||
color: var(--gray700);
|
||||
color: var(--base700);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--gray400);
|
||||
background: var(--gray50);
|
||||
border: 1px solid var(--base400);
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.active .text {
|
||||
border-color: var(--gray600);
|
||||
box-shadow: 4px 4px 4px var(--gray100);
|
||||
border-color: var(--base600);
|
||||
box-shadow: 4px 4px 4px var(--base100);
|
||||
}
|
||||
|
||||
.dragActive {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
@ -62,8 +62,8 @@ export default function TestConsole() {
|
||||
</PageHeader>
|
||||
{website && (
|
||||
<>
|
||||
<div className={classNames(styles.test, 'row')}>
|
||||
<div className="col-4">
|
||||
<Row className={styles.test}>
|
||||
<Column xs="4">
|
||||
<PageHeader>Page links</PageHeader>
|
||||
<div>
|
||||
<Link href={`/console/${websiteId}?page=1`}>
|
||||
@ -87,22 +87,22 @@ export default function TestConsole() {
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
<PageHeader>CSS events</PageHeader>
|
||||
<Button id="primary-button" className="umami--click--button-click" variant="action">
|
||||
Send event
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
<PageHeader>Javascript events</PageHeader>
|
||||
<Button id="manual-button" variant="action" onClick={handleClick}>
|
||||
Run script
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column>
|
||||
<WebsiteChart
|
||||
websiteId={website.id}
|
||||
title={website.name}
|
||||
@ -111,8 +111,8 @@ export default function TestConsole() {
|
||||
/>
|
||||
<PageHeader>Events</PageHeader>
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -1,5 +1,5 @@
|
||||
.test {
|
||||
border: 1px solid var(--gray200);
|
||||
border: 1px solid var(--base200);
|
||||
border-radius: 5px;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Column } from 'react-basics';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import Page from 'components/layout/Page';
|
||||
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
|
||||
import GridRow from 'components/layout/GridRow';
|
||||
import MenuLayout from 'components/layout/MenuLayout';
|
||||
import Link from 'components/common/Link';
|
||||
import Loading from 'components/common/Loading';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
@ -16,15 +16,13 @@ import OSTable from 'components/metrics/OSTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import ScreenTable from 'components/metrics/ScreenTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteDetails.module.css';
|
||||
import EventDataButton from 'components/common/EventDataButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
||||
@ -35,7 +33,6 @@ const messages = defineMessages({
|
||||
devices: { id: 'metrics.devices', defaultMessage: 'Devices' },
|
||||
countries: { id: 'metrics.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'metrics.languages', defaultMessage: 'Languages' },
|
||||
events: { id: 'metrics.events', defaultMessage: 'Events' },
|
||||
query: { id: 'metrics.query-parameters', defaultMessage: 'Query parameters' },
|
||||
});
|
||||
|
||||
@ -48,7 +45,6 @@ const views = {
|
||||
screen: ScreenTable,
|
||||
country: CountriesTable,
|
||||
language: LanguagesTable,
|
||||
event: EventsTable,
|
||||
query: QueryParametersTable,
|
||||
};
|
||||
|
||||
@ -56,7 +52,6 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
const { data } = useFetch(`/websites/${websiteId}`);
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [eventsData, setEventsData] = useState();
|
||||
const {
|
||||
resolve,
|
||||
query: { view },
|
||||
@ -65,7 +60,7 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
|
||||
const BackButton = () => (
|
||||
<div key="back-button" className={classNames(styles.backButton, 'col-12')}>
|
||||
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} size="small">
|
||||
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} sizes="small">
|
||||
<FormattedMessage id="label.back" defaultMessage="Back" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -107,10 +102,6 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
label: formatMessage(messages.screens),
|
||||
value: resolve({ view: 'screen' }),
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.events),
|
||||
value: resolve({ view: 'event' }),
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.query),
|
||||
value: resolve({ view: 'query' }),
|
||||
@ -137,58 +128,45 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="row">
|
||||
<div className={classNames(styles.chart, 'col')}>
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
title={data.name}
|
||||
domain={data.domain}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
stickyHeader
|
||||
/>
|
||||
{!chartLoaded && <Loading />}
|
||||
</div>
|
||||
</div>
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
title={data.name}
|
||||
domain={data.domain}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
stickyHeader
|
||||
/>
|
||||
{!chartLoaded && <Loading />}
|
||||
{chartLoaded && !view && (
|
||||
<GridLayout>
|
||||
<>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={6}>
|
||||
<Column variant="two" className={styles.column}>
|
||||
<PagesTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={6}>
|
||||
</Column>
|
||||
<Column variant="two" className={styles.column}>
|
||||
<ReferrersTable {...tableProps} />
|
||||
</GridColumn>
|
||||
</Column>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<Column variant="three" className={styles.column}>
|
||||
<BrowsersTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
</Column>
|
||||
<Column variant="three" className={styles.column}>
|
||||
<OSTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
</Column>
|
||||
<Column variant="three" className={styles.column}>
|
||||
<DevicesTable {...tableProps} />
|
||||
</GridColumn>
|
||||
</Column>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
</Column>
|
||||
<Column xs={12} sm={12} md={12} defaultSize={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</GridColumn>
|
||||
</Column>
|
||||
</GridRow>
|
||||
<GridRow className={classNames({ [styles.hidden]: !eventsData?.length > 0 })}>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<EventDataButton websiteId={websiteId} />
|
||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</GridLayout>
|
||||
</>
|
||||
)}
|
||||
{view && chartLoaded && (
|
||||
<MenuLayout
|
||||
|
@ -3,11 +3,11 @@
|
||||
}
|
||||
|
||||
.view {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -29,14 +29,3 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.eventschart {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.website {
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
margin-bottom: 30px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--gray50);
|
||||
border: 1px solid var(--gray500);
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1,7 +1,7 @@
|
||||
.username {
|
||||
border-bottom: 1px solid var(--gray500);
|
||||
border-bottom: 1px solid var(--base500);
|
||||
}
|
||||
|
||||
.username:hover {
|
||||
background: var(--gray50);
|
||||
background: var(--base50);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ CREATE TABLE event
|
||||
url String,
|
||||
referrer String,
|
||||
--event
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
event_data JSON,
|
||||
created_at DateTime('UTC')
|
||||
@ -41,6 +42,7 @@ CREATE TABLE event_queue (
|
||||
screen LowCardinality(String),
|
||||
language LowCardinality(String),
|
||||
country LowCardinality(String),
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
event_data String,
|
||||
created_at DateTime('UTC')
|
||||
@ -67,6 +69,7 @@ SELECT website_id,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
event_type,
|
||||
event_name,
|
||||
if((empty(event_data) = 0) AND startsWith(event_data, '"'), concat('{', event_data, ': true}'), event_data) AS event_data,
|
||||
created_at
|
||||
|
147
db/postgresql/migrations/01_init/migration.sql
Normal file
@ -0,0 +1,147 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"user_id" UUID NOT NULL,
|
||||
"username" VARCHAR(255) NOT NULL,
|
||||
"password" VARCHAR(60) NOT NULL,
|
||||
"role" VARCHAR(50) NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
"deleted_at" TIMESTAMPTZ(6),
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("user_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"session_id" UUID NOT NULL,
|
||||
"website_id" UUID NOT NULL,
|
||||
"hostname" VARCHAR(100),
|
||||
"browser" VARCHAR(20),
|
||||
"os" VARCHAR(20),
|
||||
"device" VARCHAR(20),
|
||||
"screen" VARCHAR(11),
|
||||
"language" VARCHAR(35),
|
||||
"country" CHAR(2),
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("session_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "website" (
|
||||
"website_id" UUID NOT NULL,
|
||||
"name" VARCHAR(100) NOT NULL,
|
||||
"domain" VARCHAR(500),
|
||||
"share_id" VARCHAR(64),
|
||||
"rev_id" INTEGER NOT NULL DEFAULT 0,
|
||||
"user_id" UUID,
|
||||
"team_id" UUID,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
"deleted_at" TIMESTAMPTZ(6),
|
||||
|
||||
CONSTRAINT "website_pkey" PRIMARY KEY ("website_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "website_event" (
|
||||
"event_id" UUID NOT NULL,
|
||||
"website_id" UUID NOT NULL,
|
||||
"session_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NOT NULL,
|
||||
"referrer" VARCHAR(500),
|
||||
"event_type" INTEGER NOT NULL DEFAULT 1,
|
||||
"event_name" VARCHAR(50),
|
||||
"event_data" JSONB,
|
||||
|
||||
CONSTRAINT "website_event_pkey" PRIMARY KEY ("event_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "team" (
|
||||
"team_id" UUID NOT NULL,
|
||||
"name" VARCHAR(50) NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
"deleted_at" TIMESTAMPTZ(6),
|
||||
|
||||
CONSTRAINT "team_pkey" PRIMARY KEY ("team_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "team_user" (
|
||||
"team_user_id" UUID NOT NULL,
|
||||
"team_id" UUID NOT NULL,
|
||||
"user_id" UUID NOT NULL,
|
||||
"role" VARCHAR(50) NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
"deleted_at" TIMESTAMPTZ(6),
|
||||
|
||||
CONSTRAINT "team_user_pkey" PRIMARY KEY ("team_user_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_user_id_key" ON "user"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_username_key" ON "user"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session_session_id_key" ON "session"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "website_website_id_key" ON "website"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "website_share_id_key" ON "website"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_created_at_idx" ON "website"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_share_id_idx" ON "website"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_created_at_idx" ON "website_event"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_session_id_idx" ON "website_event"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_website_id_idx" ON "website_event"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_website_id_created_at_idx" ON "website_event"("website_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_website_id_session_id_created_at_idx" ON "website_event"("website_id", "session_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "team_team_id_key" ON "team"("team_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "team_user_team_user_id_key" ON "team_user"("team_user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "website" ADD CONSTRAINT "website_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "team"("team_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "website" ADD CONSTRAINT "website_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_user" ADD CONSTRAINT "team_user_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "team"("team_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team_user" ADD CONSTRAINT "team_user_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddSystemUser
|
||||
INSERT INTO "user" (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa');
|
3
db/postgresql/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
@ -11,16 +11,13 @@ model User {
|
||||
id String @id @unique @map("user_id") @db.Uuid
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
isAdmin Boolean @default(false) @map("is_admin")
|
||||
role String @map("role") @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
groupRole GroupRole[]
|
||||
groupUser GroupUser[]
|
||||
userRole UserRole[]
|
||||
teamWebsite TeamWebsite[]
|
||||
teamUser TeamUser[]
|
||||
userWebsite UserWebsite[]
|
||||
website Website[]
|
||||
teamUser TeamUser[]
|
||||
Website Website[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
@ -44,19 +41,19 @@ model Session {
|
||||
|
||||
model Website {
|
||||
id String @id @unique @map("website_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
shareId String? @unique @map("share_id") @db.VarChar(64)
|
||||
revId Int @default(0) @map("rev_id") @db.Integer
|
||||
userId String? @map("user_id") @db.Uuid
|
||||
teamId String? @map("team_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
teamWebsite TeamWebsite[]
|
||||
userWebsite UserWebsite[]
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@map("website")
|
||||
@ -69,8 +66,9 @@ model WebsiteEvent {
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
eventName String @map("event_name") @db.VarChar(50)
|
||||
eventData Json @map("event_data")
|
||||
eventType Int @default(1) @map("event_type") @db.Integer
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
eventData Json? @map("event_data")
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@ -80,131 +78,31 @@ model WebsiteEvent {
|
||||
@@map("website_event")
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id() @unique() @map("group_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
groupRoles GroupRole[]
|
||||
groupUsers GroupUser[]
|
||||
|
||||
@@map("group")
|
||||
}
|
||||
|
||||
model GroupRole {
|
||||
id String @id() @unique() @map("group_role_id") @db.Uuid
|
||||
groupId String @map("group_id") @db.Uuid
|
||||
roleId String @map("role_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String? @db.Uuid
|
||||
|
||||
@@map("group_role")
|
||||
}
|
||||
|
||||
model GroupUser {
|
||||
id String @id() @unique() @map("group_user_id") @db.Uuid
|
||||
groupId String @map("group_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("group_user")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id() @unique() @map("permission_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
@@map("permission")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id() @unique() @map("role_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
groupRoles GroupRole[]
|
||||
userRoles UserRole[]
|
||||
|
||||
@@map("role")
|
||||
}
|
||||
|
||||
model UserRole {
|
||||
id String @id() @unique() @map("user_role_id") @db.Uuid
|
||||
roleId String @map("role_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_role")
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id() @unique() @map("team_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(50)
|
||||
name String @db.VarChar(50)
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
teamWebsites TeamWebsite[]
|
||||
teamUsers TeamUser[]
|
||||
teamUsers TeamUser[]
|
||||
Website Website[]
|
||||
|
||||
@@map("team")
|
||||
}
|
||||
|
||||
model TeamWebsite {
|
||||
id String @id() @unique() @map("team_website_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String? @db.Uuid
|
||||
|
||||
@@map("team_website")
|
||||
}
|
||||
|
||||
model TeamUser {
|
||||
id String @id() @unique() @map("team_user_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
role String @map("role") @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("team_user")
|
||||
}
|
||||
|
||||
model UserWebsite {
|
||||
id String @id() @unique() @map("user_website_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_website")
|
||||
}
|
||||
|
@ -12,17 +12,14 @@ export default function useRequireLogin() {
|
||||
async function loadUser() {
|
||||
setLoading(true);
|
||||
|
||||
const {
|
||||
ok,
|
||||
data: { user },
|
||||
} = await get('/auth/verify');
|
||||
const { ok, data } = await get('/auth/verify');
|
||||
|
||||
if (!ok) {
|
||||
await router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
setUser(user);
|
||||
setUser(data.user);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
5
interface/auth.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
export interface Auth {
|
||||
id: number;
|
||||
email?: string;
|
||||
teams?: string[];
|
||||
}
|
22
interface/base.d.ts
vendored
@ -1,22 +0,0 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { Auth } from './auth';
|
||||
|
||||
export interface NextApiRequestQueryBody<TQuery, TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface NextApiRequestQuery<TQuery> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
}
|
||||
|
||||
export interface NextApiRequestBody<TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface ObjectAny {
|
||||
[key: string]: any;
|
||||
}
|
22
interface/index.d.ts
vendored
@ -1,22 +0,0 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { Auth } from './auth';
|
||||
|
||||
export interface NextApiRequestQueryBody<TQuery, TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface NextApiRequestQuery<TQuery> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
}
|
||||
|
||||
export interface NextApiRequestBody<TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface ObjectAny {
|
||||
[key: string]: any;
|
||||
}
|
76
lib/auth.js
@ -1,76 +0,0 @@
|
||||
import { parseSecureToken, parseToken } from 'next-basics';
|
||||
import { getUser, getWebsite } from 'queries';
|
||||
import debug from 'debug';
|
||||
import { SHARE_TOKEN_HEADER, TYPE_USER, TYPE_WEBSITE } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
|
||||
const log = debug('umami:auth');
|
||||
|
||||
export function getAuthToken(req) {
|
||||
try {
|
||||
return req.headers.authorization.split(' ')[1];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAuthToken(req) {
|
||||
try {
|
||||
return parseSecureToken(getAuthToken(req), secret());
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseShareToken(req) {
|
||||
try {
|
||||
return parseToken(req.headers[SHARE_TOKEN_HEADER], secret());
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidToken(token, validation) {
|
||||
try {
|
||||
if (typeof validation === 'object') {
|
||||
return !Object.keys(validation).find(key => token[key] !== validation[key]);
|
||||
} else if (typeof validation === 'function') {
|
||||
return validation(token);
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function allowQuery(req, type) {
|
||||
const { id } = req.query;
|
||||
|
||||
const { user, shareToken } = req.auth;
|
||||
|
||||
if (user?.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shareToken) {
|
||||
return isValidToken(shareToken, { id });
|
||||
}
|
||||
|
||||
if (user?.id) {
|
||||
if (type === TYPE_WEBSITE) {
|
||||
const website = await getWebsite({ id });
|
||||
|
||||
return website && website.userId === user.id;
|
||||
} else if (type === TYPE_USER) {
|
||||
const user = await getUser({ id });
|
||||
|
||||
return user && user.id === id;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
129
lib/auth.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { parseSecureToken, parseToken, ensureArray } from 'next-basics';
|
||||
import debug from 'debug';
|
||||
import cache from 'lib/cache';
|
||||
import { SHARE_TOKEN_HEADER, PERMISSIONS, ROLE_PERMISSIONS } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { getTeamUser } from 'queries';
|
||||
|
||||
const log = debug('umami:auth');
|
||||
|
||||
export function getAuthToken(req) {
|
||||
try {
|
||||
return req.headers.authorization.split(' ')[1];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAuthToken(req) {
|
||||
try {
|
||||
return parseSecureToken(getAuthToken(req), secret());
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseShareToken(req) {
|
||||
try {
|
||||
return parseToken(req.headers[SHARE_TOKEN_HEADER], secret());
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidToken(token, validation) {
|
||||
try {
|
||||
if (typeof validation === 'object') {
|
||||
return !Object.keys(validation).find(key => token[key] !== validation[key]);
|
||||
} else if (typeof validation === 'function') {
|
||||
return validation(token);
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function canViewWebsite(userId: string, websiteId: string) {
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
if (website.userId) {
|
||||
return userId === website.userId;
|
||||
}
|
||||
|
||||
if (website.teamId) {
|
||||
return getTeamUser(website.teamId, userId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function canUpdateWebsite(userId: string, websiteId: string) {
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
if (website.userId) {
|
||||
return userId === website.userId;
|
||||
}
|
||||
|
||||
if (website.teamId) {
|
||||
const teamUser = await getTeamUser(website.teamId, userId);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function canDeleteWebsite(userId: string, websiteId: string) {
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
if (website.userId) {
|
||||
return userId === website.userId;
|
||||
}
|
||||
|
||||
if (website.teamId) {
|
||||
const teamUser = await getTeamUser(website.teamId, userId);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// To-do: Implement when payments are setup.
|
||||
export async function canCreateTeam(userId: string) {
|
||||
return !!userId;
|
||||
}
|
||||
|
||||
// To-do: Implement when payments are setup.
|
||||
export async function canViewTeam(userId: string, teamId) {
|
||||
return getTeamUser(teamId, userId);
|
||||
}
|
||||
|
||||
export async function canUpdateTeam(userId: string, teamId: string) {
|
||||
const teamUser = await getTeamUser(teamId, userId);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
||||
}
|
||||
|
||||
export async function canDeleteTeam(userId: string, teamId: string) {
|
||||
const teamUser = await getTeamUser(teamId, userId);
|
||||
|
||||
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
|
||||
}
|
||||
|
||||
export async function canViewUser(userId: string, viewedUserId: string) {
|
||||
return userId === viewedUserId;
|
||||
}
|
||||
|
||||
export async function canUpdateUser(userId: string, viewedUserId: string) {
|
||||
return userId === viewedUserId;
|
||||
}
|
||||
|
||||
export async function hasPermission(role: string, permission: string | string[]) {
|
||||
return ensureArray(permission).some(e => ROLE_PERMISSIONS[role]?.includes(e));
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { getWebsite, getUser, getSession } from '../queries';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
import { User, Website } from '@prisma/client';
|
||||
import redis from 'lib/redis';
|
||||
import { getSession, getUser, getWebsite } from '../queries';
|
||||
|
||||
async function fetchObject(key, query) {
|
||||
const obj = await redis.get(key);
|
||||
@ -22,10 +23,10 @@ async function storeObject(key, data) {
|
||||
}
|
||||
|
||||
async function deleteObject(key) {
|
||||
return redis.set(key, DELETED);
|
||||
return redis.set(key, redis.DELETED);
|
||||
}
|
||||
|
||||
async function fetchWebsite(id) {
|
||||
async function fetchWebsite(id): Promise<Website> {
|
||||
return fetchObject(`website:${id}`, () => getWebsite({ id }));
|
||||
}
|
||||
|
||||
@ -40,8 +41,8 @@ async function deleteWebsite(id) {
|
||||
return deleteObject(`website:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUser(id) {
|
||||
return fetchObject(`user:${id}`, () => getUser({ id }));
|
||||
async function fetchUser(id): Promise<User> {
|
||||
return fetchObject(`user:${id}`, () => getUser({ id }, true));
|
||||
}
|
||||
|
||||
async function storeUser(data) {
|
@ -106,7 +106,7 @@ function getEventDataFilterQuery(column, filters) {
|
||||
return query.join('\nand ');
|
||||
}
|
||||
|
||||
function getFilterQuery(column, filters = {}, params = []) {
|
||||
function getFilterQuery(filters = {}, params = []) {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
|
||||
@ -146,7 +146,7 @@ function getFilterQuery(column, filters = {}, params = []) {
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function parseFilters(column, filters = {}, params = []) {
|
||||
function parseFilters(filters = {}, params = []) {
|
||||
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
||||
filters;
|
||||
|
||||
@ -159,9 +159,7 @@ function parseFilters(column, filters = {}, params = []) {
|
||||
sessionFilters,
|
||||
eventFilters,
|
||||
event: { event_name },
|
||||
pageviewQuery: getFilterQuery(column, pageviewFilters, params),
|
||||
sessionQuery: getFilterQuery(column, sessionFilters, params),
|
||||
eventQuery: getFilterQuery(column, eventFilters, params),
|
||||
filterQuery: getFilterQuery(filters, params),
|
||||
};
|
||||
}
|
||||
|
||||
|
14
lib/client.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { getItem, setItem, removeItem } from 'next-basics';
|
||||
import { AUTH_TOKEN } from './constants';
|
||||
|
||||
export function getAuthToken() {
|
||||
return getItem(AUTH_TOKEN);
|
||||
}
|
||||
|
||||
export function setAuthToken(token) {
|
||||
setItem(AUTH_TOKEN, token);
|
||||
}
|
||||
|
||||
export function removeAuthToken() {
|
||||
removeItem(AUTH_TOKEN);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
export const CURRENT_VERSION = process.env.currentVersion;
|
||||
export const AUTH_TOKEN = 'umami.auth';
|
||||
export const LOCALE_CONFIG = 'umami.locale';
|
||||
@ -21,8 +22,51 @@ export const DEFAULT_WEBSITE_LIMIT = 10;
|
||||
export const REALTIME_RANGE = 30;
|
||||
export const REALTIME_INTERVAL = 3000;
|
||||
|
||||
export const TYPE_WEBSITE = 'website';
|
||||
export const TYPE_USER = 'user';
|
||||
export const EVENT_TYPE = {
|
||||
pageView: 1,
|
||||
customEvent: 2,
|
||||
};
|
||||
|
||||
export const ROLES = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
teamOwner: 'team-owner',
|
||||
teamMember: 'team-member',
|
||||
teamGuest: 'team-guest',
|
||||
};
|
||||
|
||||
export const PERMISSIONS = {
|
||||
all: 'all',
|
||||
websiteCreate: 'website:create',
|
||||
websiteUpdate: 'website:update',
|
||||
websiteDelete: 'website:delete',
|
||||
teamCreate: 'team:create',
|
||||
teamUpdate: 'team:update',
|
||||
teamDelete: 'team:delete',
|
||||
};
|
||||
|
||||
export const ROLE_PERMISSIONS = {
|
||||
[ROLES.admin]: [PERMISSIONS.all],
|
||||
[ROLES.user]: [
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
PERMISSIONS.teamCreate,
|
||||
],
|
||||
[ROLES.teamOwner]: [
|
||||
PERMISSIONS.teamUpdate,
|
||||
PERMISSIONS.teamDelete,
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamMember]: [
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamGuest]: [],
|
||||
};
|
||||
|
||||
export const THEME_COLORS = {
|
||||
light: {
|
@ -3,9 +3,10 @@ import debug from 'debug';
|
||||
import cors from 'cors';
|
||||
import { validate } from 'uuid';
|
||||
import { findSession } from 'lib/session';
|
||||
import { parseShareToken, getAuthToken } from 'lib/auth';
|
||||
import { getAuthToken, parseShareToken } from 'lib/auth';
|
||||
import { secret } from 'lib/crypto';
|
||||
import redis from 'lib/redis';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { getUser } from '../queries';
|
||||
|
||||
const log = debug('umami:middleware');
|
||||
@ -45,6 +46,10 @@ export const useAuth = createMiddleware(async (req, res, next) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.isAdmin = user.role === ROLES.admin;
|
||||
}
|
||||
|
||||
req.auth = { user, token, shareToken, key };
|
||||
next();
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import moment from 'moment-timezone';
|
||||
import debug from 'debug';
|
||||
import { PRISMA, MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
|
||||
import { FILTER_IGNORED } from 'lib/constants';
|
||||
import { PrismaClientOptions } from '@prisma/client/runtime';
|
||||
|
||||
const MYSQL_DATE_FORMATS = {
|
||||
minute: '%Y-%m-%d %H:%i:00',
|
||||
@ -37,7 +38,8 @@ function logQuery(e) {
|
||||
}
|
||||
|
||||
function getClient(options) {
|
||||
const prisma = new PrismaClient(options);
|
||||
const prisma: PrismaClient<PrismaClientOptions, 'query' | 'error' | 'info' | 'warn'> =
|
||||
new PrismaClient(options);
|
||||
|
||||
if (process.env.LOG_QUERY) {
|
||||
prisma.$on('query', logQuery);
|
||||
@ -52,7 +54,7 @@ function getClient(options) {
|
||||
return prisma;
|
||||
}
|
||||
|
||||
function getDateQuery(field, unit, timezone) {
|
||||
function getDateQuery(field, unit, timezone?): string {
|
||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
@ -73,7 +75,7 @@ function getDateQuery(field, unit, timezone) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTimestampInterval(field) {
|
||||
function getTimestampInterval(field): string {
|
||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
@ -85,7 +87,7 @@ function getTimestampInterval(field) {
|
||||
}
|
||||
}
|
||||
|
||||
function getJsonField(column, property, isNumber) {
|
||||
function getJsonField(column, property, isNumber): string {
|
||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
@ -103,7 +105,7 @@ function getJsonField(column, property, isNumber) {
|
||||
}
|
||||
}
|
||||
|
||||
function getEventDataColumnsQuery(column, columns) {
|
||||
function getEventDataColumnsQuery(column, columns): string {
|
||||
const query = Object.keys(columns).reduce((arr, key) => {
|
||||
const filter = columns[key];
|
||||
|
||||
@ -121,7 +123,7 @@ function getEventDataColumnsQuery(column, columns) {
|
||||
return query.join(',\n');
|
||||
}
|
||||
|
||||
function getEventDataFilterQuery(column, filters) {
|
||||
function getEventDataFilterQuery(column, filters): string {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
|
||||
@ -143,7 +145,7 @@ function getEventDataFilterQuery(column, filters) {
|
||||
return query.join('\nand ');
|
||||
}
|
||||
|
||||
function getFilterQuery(table, column, filters = {}, params = []) {
|
||||
function getFilterQuery(filters = {}, params = []): string {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
|
||||
@ -153,48 +155,25 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||
|
||||
switch (key) {
|
||||
case 'url':
|
||||
if (table === 'pageview' || table === 'event') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'os':
|
||||
case 'browser':
|
||||
case 'device':
|
||||
case 'country':
|
||||
if (table === 'session') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event_name':
|
||||
if (table === 'event') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
arr.push(`and ${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
break;
|
||||
|
||||
case 'referrer':
|
||||
if (table === 'pageview' || table === 'event') {
|
||||
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
||||
params.push(`%${decodeURIComponent(filter)}%`);
|
||||
}
|
||||
arr.push(`and referrer like $${params.length + 1}`);
|
||||
params.push(`%${decodeURIComponent(filter)}%`);
|
||||
break;
|
||||
|
||||
case 'domain':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
||||
arr.push(`and ${table}.referrer not like '/%'`);
|
||||
params.push(`%://${filter}/%`);
|
||||
}
|
||||
arr.push(`and referrer not like $${params.length + 1}`);
|
||||
arr.push(`and referrer not like '/%'`);
|
||||
params.push(`%://${filter}/%`);
|
||||
break;
|
||||
|
||||
case 'query':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.url like '%?%'`);
|
||||
}
|
||||
arr.push(`and url like '%?%'`);
|
||||
}
|
||||
|
||||
return arr;
|
||||
@ -203,7 +182,11 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
|
||||
function parseFilters(
|
||||
filters: { [key: string]: any } = {},
|
||||
params = [],
|
||||
sessionKey = 'session_id',
|
||||
) {
|
||||
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
||||
filters;
|
||||
|
||||
@ -218,15 +201,13 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se
|
||||
event: { event_name },
|
||||
joinSession:
|
||||
os || browser || device || country
|
||||
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
||||
? `inner join session on ${sessionKey} = session.${sessionKey}`
|
||||
: '',
|
||||
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
||||
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
||||
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
||||
filterQuery: getFilterQuery(filters, params),
|
||||
};
|
||||
}
|
||||
|
||||
async function rawQuery(query, params = []) {
|
||||
async function rawQuery(query, params = []): Promise<any> {
|
||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||
|
||||
if (db !== POSTGRESQL && db !== MYSQL) {
|
||||
@ -238,12 +219,13 @@ async function rawQuery(query, params = []) {
|
||||
return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]);
|
||||
}
|
||||
|
||||
async function transaction(queries) {
|
||||
async function transaction(queries): Promise<any> {
|
||||
return prisma.$transaction(queries);
|
||||
}
|
||||
|
||||
// Initialization
|
||||
const prisma = global[PRISMA] || getClient(PRISMA_OPTIONS);
|
||||
const prisma: PrismaClient<PrismaClientOptions, 'query' | 'error' | 'info' | 'warn'> =
|
||||
global[PRISMA] || getClient(PRISMA_OPTIONS);
|
||||
|
||||
export default {
|
||||
client: prisma,
|
32
lib/redis.js
@ -1,39 +1,39 @@
|
||||
import { createClient } from 'redis';
|
||||
import debug from 'debug';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS } from 'lib/db';
|
||||
|
||||
const log = debug('umami:redis');
|
||||
export const DELETED = 'deleted';
|
||||
const REDIS = Symbol();
|
||||
const DELETED = 'DELETED';
|
||||
|
||||
let redis;
|
||||
const enabled = Boolean(process.env.REDIS_URL);
|
||||
const url = process.env.REDIS_URL;
|
||||
const enabled = Boolean(url);
|
||||
|
||||
function getClient() {
|
||||
async function getClient() {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL, {
|
||||
retryStrategy(times) {
|
||||
log(`Redis reconnecting attempt: ${times}`);
|
||||
return 5000;
|
||||
},
|
||||
});
|
||||
const client = createClient({ url });
|
||||
client.on('error', err => log(err));
|
||||
await client.connect();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
global[REDIS] = redis;
|
||||
global[REDIS] = client;
|
||||
}
|
||||
|
||||
log('Redis initialized');
|
||||
|
||||
return redis;
|
||||
return client;
|
||||
}
|
||||
|
||||
async function get(key) {
|
||||
await connect();
|
||||
|
||||
const data = await redis.get(key);
|
||||
|
||||
try {
|
||||
return JSON.parse(await redis.get(key));
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@ -53,10 +53,10 @@ async function del(key) {
|
||||
|
||||
async function connect() {
|
||||
if (!redis && enabled) {
|
||||
redis = global[REDIS] || getClient();
|
||||
redis = global[REDIS] || (await getClient());
|
||||
}
|
||||
|
||||
return redis;
|
||||
}
|
||||
|
||||
export default { enabled, client: redis, log, connect, get, set, del };
|
||||
export default { enabled, client: redis, log, connect, get, set, del, DELETED };
|
||||
|
@ -40,7 +40,7 @@ export async function findSession(req) {
|
||||
website = await getWebsite({ id: websiteId });
|
||||
}
|
||||
|
||||
if (!website || website.isDeleted) {
|
||||
if (!website || website.deletedAt) {
|
||||
throw new Error(`Website not found: ${websiteId}`);
|
||||
}
|
||||
|
||||
|
90
lib/types.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
|
||||
export interface Auth {
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
shareToken?: string;
|
||||
}
|
||||
|
||||
export interface NextApiRequestQueryBody<TQuery = any, TBody = any> extends NextApiRequest {
|
||||
auth?: Auth;
|
||||
query: TQuery & { [key: string]: string | string[] };
|
||||
body: TBody;
|
||||
headers: any;
|
||||
}
|
||||
|
||||
export interface NextApiRequestAuth extends NextApiRequest {
|
||||
auth?: Auth;
|
||||
headers: any;
|
||||
}
|
||||
|
||||
export interface Website {
|
||||
id: string;
|
||||
userId: string;
|
||||
revId: number;
|
||||
name: string;
|
||||
domain: string;
|
||||
shareId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Share {
|
||||
id: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface Empty {}
|
||||
|
||||
export interface WebsiteActive {
|
||||
x: number;
|
||||
}
|
||||
|
||||
export interface WebsiteEventDataMetric {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export interface WebsiteMetric {
|
||||
x: string;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WebsiteEventMetric {
|
||||
x: string;
|
||||
t: string;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WebsitePageviews {
|
||||
pageviews: {
|
||||
t: string;
|
||||
y: number;
|
||||
};
|
||||
sessions: {
|
||||
t: string;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebsiteStats {
|
||||
pageviews: { value: number; change: number };
|
||||
uniques: { value: number; change: number };
|
||||
bounces: { value: number; change: number };
|
||||
totalTime: { value: number; change: number };
|
||||
}
|
||||
|
||||
export interface RealtimeInit {
|
||||
websites: Website[];
|
||||
token: string;
|
||||
data: RealtimeUpdate;
|
||||
}
|
||||
|
||||
export interface RealtimeUpdate {
|
||||
pageviews: any[];
|
||||
sessions: any[];
|
||||
events: any[];
|
||||
timestamp: number;
|
||||
}
|
10
next-env.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
@ -43,10 +43,13 @@ module.exports = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
issuer: /\.js$/,
|
||||
issuer: /\.{js|jsx|ts|tsx}$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
|
||||
|
10
package.json
@ -42,7 +42,7 @@
|
||||
"postbuild": "node scripts/postbuild.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.js": [
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint"
|
||||
],
|
||||
@ -57,6 +57,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "4.5.7",
|
||||
"@prisma/client": "4.5.0",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.3.1",
|
||||
@ -74,7 +75,6 @@
|
||||
"formik": "^2.2.9",
|
||||
"fs-extra": "^10.0.1",
|
||||
"immer": "^9.0.12",
|
||||
"ioredis": "^5.2.3",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"is-ci": "^3.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
@ -88,15 +88,17 @@
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.37.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^5.24.7",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-spring": "^9.4.4",
|
||||
"react-tooltip": "^4.2.21",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"redis": "^4.5.0",
|
||||
"request-ip": "^3.3.0",
|
||||
"semver": "^7.3.6",
|
||||
"thenby": "^1.3.4",
|
||||
|
@ -1,18 +1,17 @@
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import 'react-basics/dist/styles.css';
|
||||
import 'styles/variables.css';
|
||||
import 'styles/bootstrap-grid.css';
|
||||
import 'styles/index.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
|
||||
const client = new QueryClient();
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
const { locale, messages } = useLocale();
|
||||
const { basePath } = useRouter();
|
||||
const { dir } = useLocale();
|
||||
useConfig();
|
||||
|
||||
const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
|
||||
@ -22,22 +21,10 @@ export default function App({ Component, pageProps }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages[locale]} textComponent={Wrapper}>
|
||||
<Head>
|
||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<div className="container" dir={dir}>
|
||||
<QueryClientProvider client={client}>
|
||||
<IntlProvider locale={locale} messages={messages[locale]} textComponent={Wrapper}>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</IntlProvider>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -7,11 +7,26 @@ import {
|
||||
methodNotAllowed,
|
||||
getRandomChars,
|
||||
} from 'next-basics';
|
||||
import { getUser } from 'queries';
|
||||
import { getUser, User } from 'queries';
|
||||
import { secret } from 'lib/crypto';
|
||||
import redis from 'lib/redis';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
|
||||
export default async (req, res) => {
|
||||
export interface LoginRequestBody {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, LoginRequestBody>,
|
||||
res: NextApiResponse<LoginResponse>,
|
||||
) => {
|
||||
if (req.method === 'POST') {
|
||||
const { username, password } = req.body;
|
||||
|
||||
@ -19,7 +34,7 @@ export default async (req, res) => {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const user = await getUser({ username });
|
||||
const user = await getUser({ username }, { includePassword: true });
|
||||
|
||||
if (user && checkPassword(password, user.password)) {
|
||||
if (redis.enabled) {
|
||||
@ -27,7 +42,7 @@ export default async (req, res) => {
|
||||
|
||||
await redis.set(key, user);
|
||||
|
||||
const token = createSecureToken(key, secret());
|
||||
const token = createSecureToken({ key }, secret());
|
||||
|
||||
return ok(res, { token, user });
|
||||
}
|
@ -2,8 +2,9 @@ import { methodNotAllowed, ok } from 'next-basics';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import redis from 'lib/redis';
|
||||
import { getAuthToken } from 'lib/auth';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async (req, res) => {
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'POST') {
|
@ -1,8 +0,0 @@
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok } from 'next-basics';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
return ok(res, req.auth);
|
||||
};
|
10
pages/api/auth/verify.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { NextApiRequestAuth } from 'lib/types';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { ok } from 'next-basics';
|
||||
|
||||
export default async (req: NextApiRequestAuth, res: NextApiResponse) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
return ok(res, req.auth);
|
||||
};
|
@ -6,14 +6,41 @@ import { savePageView, saveEvent } from 'queries';
|
||||
import { useCors, useSession } from 'lib/middleware';
|
||||
import { getJsonBody, getIpAddress } from 'lib/request';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async (req, res) => {
|
||||
export interface NextApiRequestCollect extends NextApiRequest {
|
||||
session: {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
||||
await useCors(req, res);
|
||||
|
||||
if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { type, payload } = getJsonBody(req);
|
||||
|
||||
const { referrer, event_name: eventName, event_data: eventData } = payload;
|
||||
let { url } = payload;
|
||||
|
||||
// Validate eventData is JSON
|
||||
const valid = eventData && typeof eventData === 'object' && !Array.isArray(eventData);
|
||||
|
||||
if (!valid) {
|
||||
return badRequest(res, 'Event Data must be in the form of a JSON Object.');
|
||||
}
|
||||
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
const ignoreHostnames = process.env.IGNORE_HOSTNAME;
|
||||
|
||||
@ -60,10 +87,6 @@ export default async (req, res) => {
|
||||
|
||||
const session = req.session;
|
||||
|
||||
const { type, payload } = getJsonBody(req);
|
||||
|
||||
let { url, referrer, event_name: eventName, event_data: eventData } = payload;
|
||||
|
||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||
url = url.replace(/\/$/, '');
|
||||
}
|
||||
@ -74,6 +97,7 @@ export default async (req, res) => {
|
||||
await saveEvent({
|
||||
...session,
|
||||
url,
|
||||
referrer,
|
||||
eventName,
|
||||
eventData,
|
||||
});
|
@ -1,6 +1,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ok, methodNotAllowed } from 'next-basics';
|
||||
|
||||
export default async (req, res) => {
|
||||
export interface ConfigResponse {
|
||||
basePath: string;
|
||||
trackerScriptName: string;
|
||||
updatesDisabled: boolean;
|
||||
telemetryDisabled: boolean;
|
||||
adminDisabled: boolean;
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse<ConfigResponse>) => {
|
||||
if (req.method === 'GET') {
|
||||
return ok(res, {
|
||||
basePath: process.env.BASE_PATH || '',
|
@ -1,5 +0,0 @@
|
||||
import { ok } from 'next-basics';
|
||||
|
||||
export default async (req, res) => {
|
||||
return ok(res);
|
||||
};
|