feat: added a login page with Authentik authentication

This commit is contained in:
Zakaria
2026-10-16 19:35:27 -04:00
parent 668911ed6e
commit 924e354788
18 changed files with 441 additions and 54 deletions
@@ -40,7 +40,7 @@ export function EcrImageBrowser({resource, runtimeReachable}: EcrImageBrowserPro
{imagesQuery.isLoading && <div className="muted padded">Loading images</div>}
{imagesQuery.isSuccess && images.length === 0 && <div className="empty compact"><h3>No images</h3><p>Push an image to this repository to see it here.</p></div>}
{images.length > 0 && <div className="cosmos-items-table">
<table>
<table className="table ecr-images-table">
<thead><tr><th>Tags</th><th>Digest</th><th>Pushed</th><th>Size</th><th/></tr></thead>
<tbody>{images.map((image) => {
const tags = Array.isArray(image.body.imageTags) ? image.body.imageTags.filter((tag): tag is string => typeof tag === 'string') : []
+10 -1
View File
@@ -1,5 +1,5 @@
import {NavLink, Outlet, useLocation, useSearchParams} from 'react-router-dom'
import {AlertTriangle, ChevronsLeft, ChevronsRight, LayoutDashboard, Moon, Search, Sun} from 'lucide-react'
import {AlertTriangle, ChevronsLeft, ChevronsRight, LayoutDashboard, LogOut, Moon, Search, Sun} from 'lucide-react'
import {useCallback, useEffect, useRef, useState} from 'react'
import theCloud from '../../../../TheCloud.png'
import {useTheme} from '@/lib/useTheme'
@@ -220,6 +220,9 @@ export function Layout() {
</button>
<div id="topbar-status" className="topbar-status"/>
<AccountSwitcher/>
<button className="icon-btn" type="button" title="Sign out" aria-label="Sign out" onClick={() => void signOut()}>
<LogOut size={14}/>
</button>
<div className={`connection ${isConnected ? 'connected' : 'disconnected'}`}>
<span className={connectionDotClass(status)} aria-hidden="true"/>
<span className="connection-state">{connectionLabel}</span>
@@ -234,6 +237,12 @@ export function Layout() {
)
}
async function signOut(): Promise<void> {
const csrf = document.cookie.split('; ').find((item) => item.startsWith('floci_ui_csrf='))?.split('=')[1]
await fetch('/api/auth/logout', {method: 'POST', headers: csrf ? {'X-Floci-CSRF': decodeURIComponent(csrf)} : {}})
window.location.assign('/login')
}
/**
* Topbar search bar.
*
@@ -98,3 +98,23 @@ test('shows an object extension instead of a generic object type', async () => {
expect(await screen.findByText('JSON file')).toBeInTheDocument()
})
test('opens the object action with copy selected', async () => {
apiMocks.listStorageObjects.mockResolvedValue({prefix: '', objects: [{
key: 'orders/order.json', name: 'order.json', type: 'object', size: 10, lastModified: null, metadata: {},
}]})
apiMocks.listCloudResources.mockResolvedValue([bucket])
const user = userEvent.setup()
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}})
render(
<QueryClientProvider client={queryClient}>
<StorageObjectBrowser cloud="aws" resource={bucket} runtimeReachable capabilities={['copy']} onSelectObject={() => {}}/>
</QueryClientProvider>,
)
await user.click(await screen.findByRole('button', {name: 'Copy or move order.json'}))
const copyTab = screen.getAllByRole('button', {name: 'Copy'}).find((button) => button.classList.contains('drawer-tab'))
expect(copyTab).toHaveClass('active')
})
@@ -524,7 +524,7 @@ export function StorageObjectBrowser({cloud, resource, capabilities = [], runtim
</a>
)}
{copyCapability && (
<button className="icon-btn" disabled={!canCopy} title={copyCapability.reason ?? `Move / Copy ${object.name}`} onClick={() => setCopyObject(object)}>
<button className="icon-btn" disabled={!canCopy} title={copyCapability.reason ?? `Copy or move ${object.name}`} aria-label={`Copy or move ${object.name}`} onClick={() => setCopyObject(object)}>
<Copy size={13}/>
</button>
)}
@@ -606,7 +606,7 @@ function MoveOrCopyModal({
}) {
const qc = useQueryClient()
const resourceLabel = cloud === 'azure' ? 'container' : 'bucket'
const [mode, setMode] = useState<'move' | 'copy'>('move')
const [mode, setMode] = useState<'move' | 'copy'>('copy')
const [destKey, setDestKey] = useState(() => {
const parts = srcObject.key.split('/')
const filename = parts.pop() ?? srcObject.key