+1
-1
| { | ||
| "name": "rustytime", | ||
| "private": false, | ||
| "version": "0.2.3", | ||
| "version": "0.2.4", | ||
| "license": "AGPL-3.0", | ||
@@ -6,0 +6,0 @@ "type": "module", |
+7
-0
@@ -13,2 +13,8 @@ // See https://svelte.dev/docs/kit/types#app.d.ts | ||
| interface ImpersonationInfo { | ||
| admin_id: number; | ||
| admin_name: string | null; | ||
| admin_avatar_url: string | null; | ||
| } | ||
| interface Locals { | ||
@@ -19,2 +25,3 @@ auth: { | ||
| user: AuthUser | null; | ||
| impersonation: ImpersonationInfo | null; | ||
| }; | ||
@@ -21,0 +28,0 @@ } |
@@ -16,2 +16,7 @@ import type { Handle } from '@sveltejs/kit'; | ||
| }; | ||
| impersonation?: { | ||
| admin_id: number; | ||
| admin_name: string | null; | ||
| admin_avatar_url: string | null; | ||
| } | null; | ||
| } | ||
@@ -22,3 +27,4 @@ | ||
| sessionId: null, | ||
| user: null | ||
| user: null, | ||
| impersonation: null | ||
| }; | ||
@@ -54,3 +60,4 @@ | ||
| is_admin: data.user.is_admin | ||
| } | ||
| }, | ||
| impersonation: data.impersonation ?? null | ||
| }; | ||
@@ -57,0 +64,0 @@ } |
@@ -14,2 +14,3 @@ <script lang="ts"> | ||
| import LucideLogOut from '~icons/lucide/log-out'; | ||
| import LucideUserMinus from '~icons/lucide/user-minus'; | ||
| import LucideChevronsRight from '~icons/lucide/chevrons-right'; | ||
@@ -20,2 +21,3 @@ import LucideMenu from '~icons/lucide/menu'; | ||
| import UserTag from '$lib/components/ui/UserTag.svelte'; | ||
| import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; | ||
@@ -158,2 +160,3 @@ let currentTheme: 'light' | 'dark' = 'light'; | ||
| </div> | ||
| <div class="flex flex-col justify-between transition-all duration-300 mt-14 md:mt-0"> | ||
@@ -282,3 +285,28 @@ <nav class="space-y-2 flex flex-col transition-all duration-300"> | ||
| </div> | ||
| {#if $auth.impersonation && $auth.user} | ||
| <div | ||
| class="mt-4 rounded-md border border-ctp-yellow/40 bg-ctp-yellow/10 px-3 py-3 text-xs text-ctp-subtext0 transition-all duration-300" | ||
| > | ||
| {#if !collapsed} | ||
| <p class="mb-1 text-sm font-semibold text-ctp-text"> | ||
| Impersonating: {$auth.user.name || 'User'} | ||
| </p> | ||
| <p class="mb-3 text-xs"> | ||
| From: {$auth.impersonation.admin_name || 'Admin'} | ||
| </p> | ||
| {/if} | ||
| <a | ||
| href={`${PUBLIC_BACKEND_API_URL}/admin/impersonate/${$auth.impersonation.admin_id}`} | ||
| onclick={() => setTimeout(closeMobileSidebar, 100)} | ||
| class="inline-flex w-full items-center justify-center rounded-md bg-ctp-yellow px-3 py-2 text-sm font-semibold text-ctp-base transition hover:bg-ctp-yellow/80 {collapsed | ||
| ? 'px-0' | ||
| : ''}" | ||
| > | ||
| <LucideUserMinus class="h-5 w-5" /> | ||
| <span class={collapsed ? 'hidden' : 'ml-2'}>Stop impersonating</span> | ||
| </a> | ||
| </div> | ||
| {/if} | ||
| </div> | ||
| </div> |
@@ -33,2 +33,3 @@ import { browser } from '$app/environment'; | ||
| error: AuthError | null; | ||
| impersonation: App.ImpersonationInfo | null; | ||
| } | ||
@@ -41,3 +42,4 @@ | ||
| sessionId: null, | ||
| user: null | ||
| user: null, | ||
| impersonation: null | ||
| }; | ||
@@ -50,3 +52,4 @@ | ||
| isLoading: false, | ||
| error: null | ||
| error: null, | ||
| impersonation: snapshot.impersonation | ||
| }); | ||
@@ -61,3 +64,4 @@ | ||
| isLoading: true, | ||
| error: null | ||
| error: null, | ||
| impersonation: null | ||
| }); | ||
@@ -64,0 +68,0 @@ |
@@ -24,3 +24,4 @@ <script lang="ts"> | ||
| sessionId: null, | ||
| user: null | ||
| user: null, | ||
| impersonation: null | ||
| }; | ||
@@ -27,0 +28,0 @@ |
@@ -18,3 +18,4 @@ <script lang="ts"> | ||
| sessionId: null, | ||
| user: null | ||
| user: null, | ||
| impersonation: null | ||
| }; | ||
@@ -35,3 +36,4 @@ | ||
| sessionId: state.sessionId, | ||
| user: state.user | ||
| user: state.user, | ||
| impersonation: state.impersonation | ||
| }; | ||
@@ -38,0 +40,0 @@ }); |
@@ -9,2 +9,4 @@ <script lang="ts"> | ||
| import { Container, KeyValueList, PageHeading, SectionTitle, StatCard, UserTag } from '$lib'; | ||
| import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; | ||
| import { auth } from '$lib/stores/auth'; | ||
@@ -201,2 +203,5 @@ type ApexChartsConstructor = new (element: Element | string, options: ApexOptions) => ApexCharts; | ||
| > | ||
| <th class="px-6 py-3 text-left text-xs font-medium text-ctp-subtext0 uppercase" | ||
| >Actions</th | ||
| > | ||
| </tr> | ||
@@ -243,2 +248,20 @@ </thead> | ||
| </td> | ||
| <td class="px-6 py-4 whitespace-nowrap text-sm text-ctp-subtext1"> | ||
| {#if user.is_admin && (!$auth.impersonation || user.id !== $auth.impersonation.admin_id)} | ||
| <span class="text-xs uppercase tracking-wide text-ctp-subtext1/80" | ||
| >Admin</span | ||
| > | ||
| {:else} | ||
| <a | ||
| href={`${PUBLIC_BACKEND_API_URL}/admin/impersonate/${user.id}`} | ||
| class="inline-flex items-center justify-center rounded bg-ctp-lavender px-3 py-1 text-xs font-semibold text-ctp-base transition hover:bg-ctp-blue" | ||
| > | ||
| {#if $auth.impersonation && user.id === $auth.impersonation.admin_id} | ||
| Go back | ||
| {:else} | ||
| Impersonate | ||
| {/if} | ||
| </a> | ||
| {/if} | ||
| </td> | ||
| </tr> | ||
@@ -245,0 +268,0 @@ {/each} |
@@ -74,3 +74,5 @@ <script lang="ts"> | ||
| const lastUpdatedProject = $derived.by<EnhancedProject | null>(() => formattedProjects[0] ?? null); | ||
| const lastUpdatedProject = $derived.by<EnhancedProject | null>( | ||
| () => formattedProjects[0] ?? null | ||
| ); | ||
| const lastUpdatedProjectLabel = $derived.by<string>(() => { | ||
@@ -112,3 +114,2 @@ const project = formattedProjects[0]; | ||
| {/if} | ||
| </div> | ||
@@ -115,0 +116,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
136389
1.99%1214
1.42%