Compare commits

...

12 Commits

Author SHA1 Message Date
Noooste 54a5e42db6 chore: bump version to 0.2.2 and update badges in README.md
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-04-14 00:07:17 +02:00
Noooste 43ba68a4f9 feat: update directory creation to include .keep file for empty directories
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-04-13 23:42:38 +02:00
Noooste a0458fb5d0 chore: bump version to 0.2.1 and update badges in README.md
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-04-13 22:23:45 +02:00
Noste 6dc0feb374 Merge pull request #15 from Noooste/14-auth-oidc-configuration
fix: Enforce admin role
2026-04-13 21:58:15 +02:00
Noooste 33d8705431 feat: enforce admin role validation in OIDC authentication flow
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
2026-04-13 21:56:54 +02:00
Noste e7db44f45d chore: update version badges in README.md to 0.2.0
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
2026-03-07 14:41:38 +01:00
Noste 126e138382 chore: bump version to 0.2.0 and update appVersion in Chart.yaml
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
2026-03-07 14:25:00 +01:00
Noste 2efb16e248 Merge pull request #11 from Noooste/4-static-website-options
Add static option on the UI
2026-03-07 14:16:57 +01:00
Noste e800ac0ed1 Merge pull request #10 from Noooste/clean-frontend
Clean frontend
2026-03-07 11:20:08 +01:00
Noste 26ae2ef515 Merge pull request #9 from Noooste/8-feature-display-version
Add Garage and UI Version on the dashboard
2026-03-07 11:19:56 +01:00
Noste 4486697ca5 refactor: simplify access key loading in BucketSettingsDialog and enhance last modified tooltip in ObjectsTable
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
2026-03-07 11:18:14 +01:00
Noste 94ad142edf feat: add health API to retrieve application version and display in sidebar
Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com>
2026-03-07 11:13:33 +01:00
11 changed files with 119 additions and 63 deletions
+1
View File
@@ -51,6 +51,7 @@ jobs:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION=${{ steps.meta.outputs.version }}
cache-from: type=gha,scope=build-${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
outputs: type=image,name=${{ secrets.DOCKER_REGISTRY }},push-by-digest=true,name-canonical=true,push=true
+3 -1
View File
@@ -29,12 +29,14 @@ RUN --mount=type=cache,target=/go/pkg/mod \
COPY backend .
ARG VERSION=dev
RUN --mount=type=cache,target=/root/.cache/go-build \
swag init
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -installsuffix cgo -o garage-ui .
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -installsuffix cgo -ldflags "-X main.version=${VERSION}" -o garage-ui .
FROM alpine:3.23.3
+24
View File
@@ -242,6 +242,30 @@ func SetupRoutes(
})
}
// Enforce admin role if configured. Roles are often absent from the
// ID token (e.g. Keycloak only emits resource_access in access tokens
// unless the client scope is explicitly configured), so fall back to
// the userinfo endpoint before denying access.
if cfg.Auth.OIDC.AdminRole != "" {
if !authService.IsAdmin(userInfo) {
if uiFromUserinfo, uiErr := authService.GetUserInfo(ctx, token); uiErr == nil {
if len(uiFromUserinfo.Roles) > 0 {
userInfo.Roles = uiFromUserinfo.Roles
}
}
if !authService.IsAdmin(userInfo) {
logger.Warn().
Str("username", userInfo.Username).
Str("required_role", cfg.Auth.OIDC.AdminRole).
Strs("roles", userInfo.Roles).
Msg("OIDC login denied: user does not have required admin role")
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "User does not have the required admin role",
})
}
}
}
// Generate JWT session token
sessionToken, err := authService.GenerateSessionToken(userInfo)
if err != nil {
+1 -1
View File
@@ -54,7 +54,7 @@ import (
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
const version = "0.1.0"
var version = "dev"
func main() {
// Parse command-line flags
@@ -10,8 +10,8 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { accessApi } from '@/lib/api';
import type { AccessKey, Bucket } from '@/types';
import { useAccessKeys } from '@/hooks/useApi';
import type { Bucket } from '@/types';
import { toast } from 'sonner';
interface BucketSettingsDialogProps {
@@ -22,7 +22,7 @@ interface BucketSettingsDialogProps {
}
export function BucketSettingsDialog({ open, onOpenChange, bucket, onGrantPermission }: BucketSettingsDialogProps) {
const [availableKeys, setAvailableKeys] = useState<AccessKey[]>([]);
const { data: availableKeys = [] } = useAccessKeys();
const [selectedAccessKey, setSelectedAccessKey] = useState<string>('');
const [permissionRead, setPermissionRead] = useState(false);
const [permissionWrite, setPermissionWrite] = useState(false);
@@ -30,20 +30,10 @@ export function BucketSettingsDialog({ open, onOpenChange, bucket, onGrantPermis
useEffect(() => {
if (open && bucket) {
loadAccessKeys();
resetForm();
}
}, [open, bucket]);
const loadAccessKeys = async () => {
try {
const keys = await accessApi.listKeys();
setAvailableKeys(keys);
} catch (error) {
console.error('Failed to load access keys:', error);
}
};
const resetForm = () => {
setSelectedAccessKey('');
setPermissionRead(false);
@@ -301,56 +301,60 @@ export function ObjectsTable({
</TableCell>
<TableCell>{obj.isFolder ? null : formatBytes(obj.size)}</TableCell>
<TableCell>
{obj.lastModified ?
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="decoration-dashed decoration-1 underline underline-offset-6 cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
{new Date(obj.lastModified).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
})} {new Date(obj.lastModified).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})} CET
</div>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1 min-w-max">
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">UTC</span>
<span className="text-sm text-white">
{new Date(obj.lastModified).toLocaleString('en-GB', {
{obj.lastModified ? (() => {
const d = new Date(obj.lastModified);
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="decoration-dashed decoration-1 underline underline-offset-6 cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
{d.toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
})} {d.toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'UTC',
})} UTC
</span>
</div>
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">Relative</span>
<span className="text-sm text-white">
{formatRelativeTime(new Date(obj.lastModified))}
</span>
</div>
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">Timestamp</span>
<span className="text-sm text-white font-mono">
{new Date(obj.lastModified).toISOString()}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>: null}
})} CET
</div>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1 min-w-max">
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">UTC</span>
<span className="text-sm text-white">
{d.toLocaleString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'UTC',
})} UTC
</span>
</div>
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">Relative</span>
<span className="text-sm text-white">
{formatRelativeTime(d)}
</span>
</div>
<div className="flex gap-3 items-center">
<span className="text-sm text-gray-400 w-20 text-right">Timestamp</span>
<span className="text-sm text-white font-mono">
{d.toISOString()}
</span>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
})() : null}
</TableCell>
<TableCell>
{!obj.isFolder && (
@@ -3,6 +3,8 @@ import {cn} from '@/lib/utils';
import {Database, Key, LayoutDashboard, LogOut, Server, User} from 'lucide-react';
import {useAuthStore} from '@/store/auth-store';
import {Button} from '@/components/ui/button';
import { useQuery } from '@tanstack/react-query';
import { healthApi, garageApi } from '@/lib/api';
interface NavItem {
title: string;
@@ -46,6 +48,24 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
logout();
};
const { data: uiVersion } = useQuery({
queryKey: ['ui-version'],
queryFn: healthApi.getVersion,
staleTime: 5 * 60 * 1000,
retry: false,
});
const { data: nodeInfo } = useQuery({
queryKey: ['garage-version'],
queryFn: () => garageApi.getNodeInfo('self'),
staleTime: 5 * 60 * 1000,
retry: false,
});
const garageVersion = nodeInfo
? Object.values(nodeInfo.success)[0]?.garageVersion
: undefined;
return (
<div
className={cn(
@@ -107,6 +127,13 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
</Button>
</div>
)}
{(uiVersion || garageVersion) && (
<div className="px-4 pb-3 text-xs text-muted-foreground text-center">
{uiVersion && `UI ${uiVersion}`}
{uiVersion && garageVersion && ' | '}
{garageVersion && `Garage ${garageVersion}`}
</div>
)}
</div>
);
}
+1 -1
View File
@@ -200,7 +200,7 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
try {
const dirKey = currentPath ? `${currentPath}${dirName}/` : `${dirName}/`;
await objectsApi.upload(bucketName, dirKey, new File([], ''));
await objectsApi.upload(bucketName, dirKey, new File([], '.keep'));
toast.success(`Directory "${dirName}" created successfully`);
await fetchObjects(currentContinuationToken, true);
return true;
+8
View File
@@ -159,6 +159,14 @@ export const authApi = {
},
};
// Health API
export const healthApi = {
getVersion: async (): Promise<string> => {
const response = await api.get('/v1/health');
return response.data.data.version as string;
},
};
// Bucket API
export const bucketsApi = {
list: async (): Promise<Bucket[]> => {
+2 -2
View File
@@ -3,8 +3,8 @@ name: garage-ui
description: A Helm chart for Garage UI - Web interface for Garage S3 object storage
icon: https://helm.noste.dev/garage.png
type: application
version: 0.1.15
appVersion: "v0.1.15"
version: 0.2.2
appVersion: "v0.2.2"
keywords:
- garage
- s3
+2 -2
View File
@@ -2,8 +2,8 @@
A Helm chart for deploying [Garage UI](https://github.com/Noooste/garage-ui), a modern web interface for managing [Garage](https://garagehq.deuxfleurs.fr/) distributed object storage systems.
[![Version](https://img.shields.io/badge/version-0.1.13-blue.svg)](Chart.yaml)
[![App Version](https://img.shields.io/badge/app%20version-v0.1.2-green.svg)](Chart.yaml)
[![Version](https://img.shields.io/badge/version-0.2.2-blue.svg)](Chart.yaml)
[![App Version](https://img.shields.io/badge/app%20version-v0.2.2-green.svg)](Chart.yaml)
## Table of Contents