diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-06-30 01:04:48 +0200 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-06-30 01:04:48 +0200 |
| commit | 6f16b7ca72899e2ae81f4669cdf1b10a43c692e7 (patch) | |
| tree | d2b1e249a0a9e953316dac9bfbcd415eda893e92 | |
| parent | a19e31557f6ef33ed33d694968abe7416e878c60 (diff) | |
| download | greatoffice-6f16b7ca72899e2ae81f4669cdf1b10a43c692e7.tar.xz greatoffice-6f16b7ca72899e2ae81f4669cdf1b10a43c692e7.zip | |
latest from desktop
| -rw-r--r-- | apps/projects/src/_assets/projects.png | bin | 0 -> 7951 bytes | |||
| -rw-r--r-- | apps/projects/src/app/index.scss | 1 | ||||
| -rw-r--r-- | apps/projects/src/app/pages/_layout.svelte | 200 | ||||
| -rw-r--r-- | apps/web-shared/src/assets/logos/projects.png | bin | 0 -> 7951 bytes | |||
| -rw-r--r-- | apps/web-shared/src/styles/components/side-navigation.scss | 233 | ||||
| -rw-r--r-- | apps/web-shared/src/styles/components/vanilla-responsive-sidebar.scss | 146 | ||||
| -rw-r--r-- | server/src/Jobs/TokenCleanupJob.cs | 5 | ||||
| -rw-r--r-- | server/src/Jobs/VaultTokenRenewalJob.cs | 15 | ||||
| -rw-r--r-- | server/src/Program.cs | 6 | ||||
| -rw-r--r-- | server/src/Services/VaultService.cs | 20 |
10 files changed, 564 insertions, 62 deletions
diff --git a/apps/projects/src/_assets/projects.png b/apps/projects/src/_assets/projects.png Binary files differnew file mode 100644 index 0000000..e49191f --- /dev/null +++ b/apps/projects/src/_assets/projects.png diff --git a/apps/projects/src/app/index.scss b/apps/projects/src/app/index.scss index 0892d63..b47151f 100644 --- a/apps/projects/src/app/index.scss +++ b/apps/projects/src/app/index.scss @@ -37,3 +37,4 @@ @use '../../web-shared/src/styles/components/menu'; @use '../../web-shared/src/styles/components/user-menu'; @use '../../web-shared/src/styles/components/light-dark-switch'; +@use '../../web-shared/src/styles/components/side-navigation'; diff --git a/apps/projects/src/app/pages/_layout.svelte b/apps/projects/src/app/pages/_layout.svelte index f725397..594fe9e 100644 --- a/apps/projects/src/app/pages/_layout.svelte +++ b/apps/projects/src/app/pages/_layout.svelte @@ -25,56 +25,158 @@ <ProfileModal bind:functions={ProfileModalFunctions}/> <BlowoutToolbelt/> -<nav class="container max-width-xl@md width-fit-content@md width-100% max-width-none margin-y-xs@md margin-bottom-xs block@md position-relative@md position-fixed z-index-fixed-element bottom-unset@md bottom-0"> - <div class="tabs-nav-v2 justify-between"> - <div class="tab-v2"> - <div class="tab-v2"> - <a href="/home" - use:link - class="tabs-nav-v2__item {($location === '/' || $location.startsWith('/home')) ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.home()}</a> - </div> - <div class="tab-v2"> - <a href="/data" - use:link - class="tabs-nav-v2__item {$location.startsWith('/data') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.data()}</a> - </div> - <div class="tab-v2"> - <a href="/settings" - use:link - class="tabs-nav-v2__item {$location.startsWith('/settings') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.settings()}</a> +<header class="position-sticky top-0 z-index-header bg shadow-xs padding-x-component padding-y-sm flex items-center justify-between hide@md"> + <a class="block size-32 hover:reduce-opacity" + href="#"> + <svg class="block" + viewBox="0 0 40 40"> + <circle fill="var(--color-contrast-higher)" + cx="20" + cy="20" + r="20"/> + <path d="M12.64,20.564a6.437,6.437,0,0,0-4.4,1.388S10,24.2,12.133,24.475a6.486,6.486,0,0,0,3.625-.846S14.455,20.8,12.64,20.564Z" + fill="var(--color-bg)"/> + <path d="M22.036,13.407a7.041,7.041,0,0,0-1.111-3.88s-3,1.562-3.152,3.54a6.978,6.978,0,0,0,1.739,4.688S21.851,15.73,22.036,13.407Z" + fill="var(--color-bg)"/> + <path d="M29.048,26.433a7.624,7.624,0,0,0-.321-4.122c-1.052-2.448-4.326-3.784-4.326-3.784a7.973,7.973,0,0,0-.164,5.713A3.294,3.294,0,0,0,25.451,25.6,16.016,16.016,0,0,1,14.758,10.527v-1h-2v1A17.988,17.988,0,0,0,21.19,25.746a5.859,5.859,0,0,0-2.433-.151,8.093,8.093,0,0,0-4,2.352s2.6,2.883,4.846,2.49a7.889,7.889,0,0,0,4.627-3.153,17.885,17.885,0,0,0,6.527,1.243h1v-2h-1A16.094,16.094,0,0,1,29.048,26.433Z" + fill="var(--color-bg)"/> + </svg> + </a> + + <button class="btn btn--subtle">Menu</button> +</header> + +<div class="flex@md"> + <aside class="sidebar sidebar--static@md sidebar--is-visible" + data-static-class="position-relative z-index-2 bg-dark flex flex-column"> + <div class="sidebar__panel flex-grow flex flex-column"> + <header class="sidebar__header bg padding-y-sm padding-left-md padding-right-sm border-bottom z-index-2"> + <h1 class="text-md text-truncate">Menu</h1> + + <button class="reset sidebar__close-btn"> + <svg class="icon icon--xs" + viewBox="0 0 16 16"><title>Close panel</title> + <g stroke-width="2" + stroke="currentColor" + fill="none" + stroke-linecap="round" + stroke-linejoin="round" + stroke-miterlimit="10"> + <line x1="13.5" + y1="2.5" + x2="2.5" + y2="13.5"></line> + <line x1="2.5" + y1="2.5" + x2="13.5" + y2="13.5"></line> + </g> + </svg> + </button> + </header> + + <header class="display@md padding-x-xs margin-bottom-lg"> + <div class="flex items-center justify-between padding-right-xs padding-left-xxxs"> + <a class="block hover:reduce-opacity width-xl" + href="#"> + <img src="/_assets/projects.png" + alt="Projects from Greatoffice"> + </a> + </div> + </header> + + <div class="position-relative z-index-1"> + <nav class="sidenav-v4 padding-x-xs padding-bottom-xs"> + <ul> + <li class="sidenav-v4__item"> + <a href="/home" + use:link + class="sidenav-v4__link {($location === '/' || $location.startsWith('/home')) ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.home()}</a> + </li> + <li class="sidenav-v4__item"> + <a href="/data" + use:link + class="sidenav-v4__link {$location.startsWith('/data') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.data()}</a> + </li> + <li class="sidenav-v4__item"> + <a href="/settings" + use:link + class="sidenav-v4__link {$location.startsWith('/settings') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.settings()}</a> + </li> + </ul> + </nav> + + <div class="padding-x-xs padding-bottom-sm hide@md"> + <div class="padding-x-sm"> + <div class="search-input search-input--icon-left text-md"> + <input class="search-input__input form-control" + type="search" + name="search-input" + id="search-input" + placeholder="Search..." + aria-label="Search"> + <button class="search-input__btn"> + <svg class="icon" + viewBox="0 0 20 20"><title>Submit</title> + <g fill="none" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2"> + <circle cx="8" + cy="8" + r="6"/> + <line x1="12.242" + y1="12.242" + x2="18" + y2="18"/> + </g> + </svg> + </button> + </div> + </div> + </div> </div> + + <footer class="sidebar__footer border-top border-alpha padding-xs margin-top-auto position-sticky bottom-0 z-index-1"> + </footer> </div> - <div class="tab-v2 padding-x-sm"> - <Button class="user-menu-control" - variant="reset" - id="open-user-menu" - on:click={() => showUserMenu = !showUserMenu} - text={username} - icon={IconNames.chevronDown} - icon_width="2rem" - icon_height="2rem" - icon_right_aligned="true" - title="{$LL.nav.usermenu.toggleTitle()}" - aria-controls="{userMenuId}" - /> - <Menu bind:show="{showUserMenu}" - trigger={userMenuTriggerNode} - id="{userMenuId}"> - <div slot="options"> - <MenuItem on:click={() => ProfileModalFunctions.open()}> - <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span> - </MenuItem> - <MenuItemSeparator/> - <MenuItem danger="true" - on:click={() => logout_user()}> - <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span> - </MenuItem> - </div> - </Menu> + </aside> + + <nav class="container max-width-xl@md width-fit-content@md width-100% max-width-none margin-y-xs@md margin-bottom-xs block@md position-relative@md position-fixed z-index-fixed-element bottom-unset@md bottom-0 is-hidden"> + <div class="tabs-nav-v2 justify-between"> + <div class="tab-v2 padding-x-sm"> + <Button class="user-menu-control" + variant="reset" + id="open-user-menu" + on:click={() => showUserMenu = !showUserMenu} + text={username} + icon={IconNames.chevronDown} + icon_width="2rem" + icon_height="2rem" + icon_right_aligned="true" + title="{$LL.nav.usermenu.toggleTitle()}" + aria-controls="{userMenuId}" + /> + <Menu bind:show="{showUserMenu}" + trigger={userMenuTriggerNode} + id="{userMenuId}"> + <div slot="options"> + <MenuItem on:click={() => ProfileModalFunctions.open()}> + <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span> + </MenuItem> + <MenuItemSeparator/> + <MenuItem danger="true" + on:click={() => logout_user()}> + <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span> + </MenuItem> + </div> + </Menu> + </div> </div> - </div> -</nav> + </nav> -<main class="container max-width-xl"> - <slot/> -</main> + <main class="container max-width-xl position-relative z-index-1 flex-grow min-height-100vh position-sticky@md top-0@md height-100vh@md overflow-auto@md"> + <slot/> + </main> +</div> diff --git a/apps/web-shared/src/assets/logos/projects.png b/apps/web-shared/src/assets/logos/projects.png Binary files differnew file mode 100644 index 0000000..e49191f --- /dev/null +++ b/apps/web-shared/src/assets/logos/projects.png diff --git a/apps/web-shared/src/styles/components/side-navigation.scss b/apps/web-shared/src/styles/components/side-navigation.scss new file mode 100644 index 0000000..0b30c7b --- /dev/null +++ b/apps/web-shared/src/styles/components/side-navigation.scss @@ -0,0 +1,233 @@ +@use '../base' as *; +@use 'vanilla-responsive-sidebar' as *; + +/* -------------------------------- + +File#: _2_side-navigation-v4 +Title: Side Navigation v4 +Descr: Main, side navigation +Usage: codyhouse.co/license + +-------------------------------- */ + +.sidenav-v4 { + --sidenav-v4-icon-size: 20px; + --sidenav-v4-icon-margin-right: var(--space-xxs); +} + +.sidenav-v4__item { + position: relative; +} + +.sidenav-v4__link, +.sidenav-v4__sub-link, +.sidenav-v4__separator { + padding: var(--space-sm); +} + +.sidenav-v4__link, .sidenav-v4__sub-link { + display: flex; + align-items: center; + + width: 100%; + border-radius: var(--radius-md); + + text-decoration: none; + color: inherit; + line-height: 1; + font-size: var(--text-md); + + transition: .2s; + + &:hover { + color: var(--color-primary); + background-color: alpha(var(--color-contrast-higher), 0.075); + } + + &[aria-current="page"] { + color: var(--color-primary); + } +} + +.sidenav-v4__sub-link { + position: relative; + color: var(--color-contrast-medium); + + /* dot indicator */ + &::before { + content: ''; + display: block; + --size: 6px; + width: var(--size); + height: var(--size); + background: currentColor; + border-radius: 50%; + margin-left: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2); + margin-right: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2 + var(--sidenav-v4-icon-margin-right)); + + opacity: 0; /* visible only if current */ + } + + &[aria-current="page"] { + &::before { /* show dot indicator */ + opacity: 1; + } + } +} + +.sidenav-v4__notification-marker { + margin-left: auto; + background-color: var(--color-accent); + border-radius: var(--radius-md); + + height: 16px; + line-height: 16px; + padding: 0 4px; + color: var(--color-white); + font-size: 12px; + + /* hide - visible only on desktop */ + display: none; +} + +/* label icon */ +.sidenav-v4__icon { + --size: var(--sidenav-v4-icon-size); + margin-right: var(--sidenav-v4-icon-margin-right); +} + +/* arrow icon - visible on mobile if item is expandable */ +.sidenav-v4__arrow-icon { + --size: 20px; + + .icon__group { + will-change: transform; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: transform .3s var(--ease-out); + + > * { + transform-origin: 50% 50%; + stroke-dasharray: 20; + stroke-dashoffset: 0; + transform: translateY(0px); + transition: transform .3s, stroke-dashoffset .3s; + transition-timing-function: var(--ease-out); + } + + .sidenav-v4__item--collapsed & { + transform: rotate(0deg); + + > * { + transform: translateY(4px); + } + + > *:first-child { + stroke-dashoffset: 10.15; + } + + > *:last-child { + stroke-dashoffset: 10.15; + } + } + } + +} + +/* current item */ +.sidenav-v4__item--current { + .sidenav-v4__sub-list { + display: block; /* show sublist */ + } +} + +/* separator */ +.sidenav-v4__separator { + span { + display: block; + width: var(--sidenav-v4-icon-size); + height: 1px; + background-color: var(--color-contrast-lower); + } +} + +/* mobile only */ +@include breakpoint(md, "not all") { + .sidenav-v4__item--collapsed { + .sidenav-v4__sub-list { + display: none; + } + } + + .sidenav-v4__link--href { + display: none; /* hide link -> show button */ + } +} + +/* desktop */ +@include breakpoint(md) { + .sidenav-v4__sub-list { + display: none; + } + + .sidenav-v4__link, + .sidenav-v4__sub-link, + .sidenav-v4__separator { + padding: var(--space-xs); + } + + .sidenav-v4__link, + .sidenav-v4__sub-link { + font-size: var(--text-base); + } + + .sidenav-v4__link--btn { + display: none; /* hide button -> show link */ + } + + /* tooltip */ + .sidenav-v4__item:not(.sidenav-v4__item--current) { + .sidenav-v4__sub-list { + width: 220px; + position: absolute; + z-index: var(--z-index-overlay); + left: 100%; + top: 0; + + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-md); + border-radius: var(--radius-md); + + overflow: hidden; + } + + .sidenav-v4__sub-link { + border-radius: 0; + color: var(--color-contrast-high); + + &::before { + display: none; /* remove dot indicator */ + } + + &:hover { + color: var(--color-primary); + } + } + + &.sidenav-v4__item--hover, &:focus-within { + .sidenav-v4__sub-list { + display: block; + } + } + + &:hover .sidenav-v4__link { /* highlight main link if tooltip is visible */ + color: var(--color-primary); + background-color: alpha(var(--color-contrast-higher), 0.075); + } + } + + /* notification marker */ + .sidenav-v4__notification-marker { + display: block; + } +} diff --git a/apps/web-shared/src/styles/components/vanilla-responsive-sidebar.scss b/apps/web-shared/src/styles/components/vanilla-responsive-sidebar.scss new file mode 100644 index 0000000..735cc1e --- /dev/null +++ b/apps/web-shared/src/styles/components/vanilla-responsive-sidebar.scss @@ -0,0 +1,146 @@ +@use '../base' as *; + +/* -------------------------------- + +File#: _1_responsive-sidebar +Title: Responsive Sidebar +Descr: Responsive sidebar container +Usage: codyhouse.co/license + +-------------------------------- */ + +/* mobile version only (--default) 👇 */ +.sidebar:not(.sidebar--static) { + position: fixed; + top: 0; + left: 0; + z-index: var(--z-index-fixed-element, 10); + width: 100%; + height: 100%; + visibility: hidden; + transition: visibility 0s 0.3s; + + &::after { /* overlay layer */ + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: alpha(var(--color-black), 0); + transition: background-color .3s; + z-index: 1; + } + + .sidebar__panel { /* content */ + position: absolute; + top: 0; + left: 0; + z-index: 2; + width: 100%; + max-width: 380px; + height: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; + background-color: var(--color-bg); + transform: translateX(-100%); + transition: box-shadow 0.3s, transform 0.3s; + } + + &.sidebar--right-on-mobile { + .sidebar__panel { + left: auto; + right: 0; + transform: translateX(100%); + } + } + + &.sidebar--is-visible { + visibility: visible; + transition: none; + + &::after { + background-color: alpha(var(--color-black), 0.85); + } + + .sidebar__panel { + transform: translateX(0); + box-shadow: var(--shadow-md); + } + } +} + +/* end mobile version */ + +.sidebar__header { + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; +} + +.sidebar__close-btn { + --size: 32px; + width: var(--size); + height: var(--size); + display: flex; + border-radius: 50%; + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-sm); + transition: .2s; + flex-shrink: 0; + + .icon { + display: block; + margin: auto; + } + + &:hover { + background-color: var(--color-bg-lighter); + box-shadow: var(--inner-glow), var(--shadow-md); + } +} + +.sidebar__footer { + background-color: var(--color-bg); +} + +/* desktop version only (--static) 👇 */ +.sidebar--static { + flex-shrink: 0; + flex-grow: 1; + width: 100%; + max-width: 320px; + + .sidebar__header { + display: none; + } + + .sidebar__footer { + background-color: var(--color-bg-dark); + } +} + +.sidebar--sticky-on-desktop { + position: sticky; + top: var(--space-sm); + max-height: calc(100vh - var(--space-sm)); + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +/* end desktop version */ + +.sidebar--static::before { + content: 'static'; +} + +@each $breakpoint, $value in $breakpoints { + .sidebar--static\@#{$breakpoint}::before { + content: 'mobile'; + @include breakpoint(#{$breakpoint}) { + content: 'static'; + } + } +} diff --git a/server/src/Jobs/TokenCleanupJob.cs b/server/src/Jobs/TokenCleanupJob.cs index 3b042b3..fce40c9 100644 --- a/server/src/Jobs/TokenCleanupJob.cs +++ b/server/src/Jobs/TokenCleanupJob.cs @@ -13,9 +13,10 @@ public class TokenCleanupJob : IJob } public Task Execute(IJobExecutionContext context) { - var staleTokens = _context.AccessTokens.Where(c => c.ExpiryDate < AppDateTime.UtcNow); + var staleTokens = _context.AccessTokens.Where(c => c.ExpiryDate < AppDateTime.UtcNow).ToList(); + if (staleTokens.IsNullOrEmpty()) return Task.CompletedTask; _logger.LogInformation("Removing {0} stale tokens", staleTokens.Count()); - _context.AccessTokens.RemoveRange(); + _context.AccessTokens.RemoveRange(staleTokens); return Task.CompletedTask; } } diff --git a/server/src/Jobs/VaultTokenRenewalJob.cs b/server/src/Jobs/VaultTokenRenewalJob.cs new file mode 100644 index 0000000..fffbf7c --- /dev/null +++ b/server/src/Jobs/VaultTokenRenewalJob.cs @@ -0,0 +1,15 @@ +using Quartz; + +namespace IOL.GreatOffice.Api.Jobs; + +public class VaultTokenRenewalJob : IJob +{ + private readonly ILogger<VaultTokenRenewalJob> _logger; + public VaultTokenRenewalJob(ILogger<VaultTokenRenewalJob> logger) { + _logger = logger; + } + + public Task Execute(IJobExecutionContext context) { + return Task.CompletedTask; + } +} diff --git a/server/src/Program.cs b/server/src/Program.cs index b7e6ce6..d7bbf9f 100644 --- a/server/src/Program.cs +++ b/server/src/Program.cs @@ -38,6 +38,7 @@ global using IOL.GreatOffice.Api.Data.Static; global using IOL.GreatOffice.Api.Services; global using IOL.GreatOffice.Api.Utilities; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using IOL.GreatOffice.Api.Endpoints.V1; using IOL.GreatOffice.Api.Jobs; using Microsoft.AspNetCore.HttpOverrides; @@ -89,7 +90,10 @@ public static class Program }); } - builder.Services.AddDataProtection().PersistKeysToDbContext<AppDbContext>(); + builder.Services + .AddDataProtection() + .PersistKeysToDbContext<AppDbContext>() + .ProtectKeysWithCertificate(vaultService.Get<X509Certificate2>("")); builder.Services.Configure(JsonSettings.Default); builder.Services.AddQuartz(options => { options.UsePersistentStore(o => { diff --git a/server/src/Services/VaultService.cs b/server/src/Services/VaultService.cs index 6034586..f6d0ad8 100644 --- a/server/src/Services/VaultService.cs +++ b/server/src/Services/VaultService.cs @@ -15,7 +15,7 @@ public class VaultService CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12); if (token.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_TOKEN is empty"); if (vaultUrl.IsNullOrWhiteSpace()) throw new ApplicationException("VAULT_URL is empty"); - client.DefaultRequestHeaders.Add(AppHeaders.VAULT_TOKEN, token); + client.DefaultRequestHeaders.Add("X-Vault-Token", token); client.BaseAddress = new Uri(vaultUrl); _client = client; _cache = cache; @@ -29,17 +29,17 @@ public class VaultService cacheEntry => { cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL); var getSecretResponse = _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path).Result; - if (getSecretResponse != null) { - Log.Debug("Setting new Vault cache, " - + new { - PATH = path, - CACHE_TTL, - Data = JsonSerializer.Serialize(getSecretResponse.Data.Data) - }); - return getSecretResponse.Data.Data ?? default; + if (getSecretResponse == null) { + return default; } - return default; + Log.Debug("Setting new Vault cache, " + + new { + PATH = path, + CACHE_TTL, + Data = JsonSerializer.Serialize(getSecretResponse.Data.Data) + }); + return getSecretResponse.Data.Data ?? default; }); } |
