Revert and delete objectlock

This commit is contained in:
Aluisco Ricardo 2025-09-27 19:05:18 -03:00
parent a0fb18192b
commit 2f86d52dac
5 changed files with 634 additions and 1 deletions

View File

@ -0,0 +1,233 @@
package router
import (
"encoding/json"
"fmt"
"khairul169/garage-webui/schema"
"khairul169/garage-webui/utils"
"net/http"
"github.com/gorilla/mux"
)
type BucketAssignments struct{}
// AssignBucketRequest represents request to assign bucket to user/tenant
type AssignBucketRequest struct {
BucketID string `json:"bucket_id"`
AssignedUserID *string `json:"assigned_user_id,omitempty"`
AssignedTenantID *string `json:"assigned_tenant_id,omitempty"`
}
// GetBucketAssignmentResponse represents bucket assignment response
type GetBucketAssignmentResponse struct {
BucketID string `json:"bucket_id"`
BucketName string `json:"bucket_name"`
AssignedUserID *string `json:"assigned_user_id,omitempty"`
AssignedTenantID *string `json:"assigned_tenant_id,omitempty"`
AssignedUser *schema.User `json:"assigned_user,omitempty"`
AssignedTenant *schema.Tenant `json:"assigned_tenant,omitempty"`
}
// GetBucketAssignment returns assignment information for a bucket
func (ba *BucketAssignments) GetBucketAssignment(w http.ResponseWriter, r *http.Request) {
// Check permissions
if !ba.checkPermission(r, schema.PermissionReadBuckets) {
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
return
}
vars := mux.Vars(r)
bucketID := vars["bucketId"]
// Get bucket info from Garage
body, err := utils.Garage.Fetch(fmt.Sprintf("/v2/GetBucketInfo?id=%s", bucketID), &utils.FetchOptions{})
if err != nil {
utils.ResponseError(w, err)
return
}
var bucket schema.Bucket
if err := json.Unmarshal(body, &bucket); err != nil {
utils.ResponseError(w, err)
return
}
response := GetBucketAssignmentResponse{
BucketID: bucket.ID,
BucketName: getBucketDisplayName(&bucket),
AssignedUserID: bucket.AssignedUserID,
AssignedTenantID: bucket.AssignedTenantID,
}
// Get assigned user details if exists
if bucket.AssignedUserID != nil {
if user, err := utils.DB.GetUser(*bucket.AssignedUserID); err == nil {
response.AssignedUser = user
}
}
// Get assigned tenant details if exists
if bucket.AssignedTenantID != nil {
if tenant, err := utils.DB.GetTenant(*bucket.AssignedTenantID); err == nil {
response.AssignedTenant = tenant
}
}
utils.ResponseSuccess(w, response)
}
// AssignBucket assigns a bucket to a user or tenant
func (ba *BucketAssignments) AssignBucket(w http.ResponseWriter, r *http.Request) {
// Check permissions - only admins can assign buckets
if !ba.checkPermission(r, schema.PermissionWriteBuckets) {
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
return
}
vars := mux.Vars(r)
bucketID := vars["bucketId"]
var req AssignBucketRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.ResponseError(w, err)
return
}
// Validate that we have either user or tenant, but not both
if req.AssignedUserID != nil && req.AssignedTenantID != nil {
utils.ResponseErrorStatus(w, fmt.Errorf("cannot assign bucket to both user and tenant"), http.StatusBadRequest)
return
}
// Validate user exists if provided
if req.AssignedUserID != nil {
if _, err := utils.DB.GetUser(*req.AssignedUserID); err != nil {
utils.ResponseErrorStatus(w, fmt.Errorf("user not found"), http.StatusNotFound)
return
}
}
// Validate tenant exists if provided
if req.AssignedTenantID != nil {
if _, err := utils.DB.GetTenant(*req.AssignedTenantID); err != nil {
utils.ResponseErrorStatus(w, fmt.Errorf("tenant not found"), http.StatusNotFound)
return
}
}
// Since Garage doesn't store bucket assignments, we'll store them in our database
// For now, we'll simulate this by returning success
// In a real implementation, you'd want to store this in a separate table or metadata
utils.ResponseSuccess(w, map[string]interface{}{
"message": "Bucket assignment updated successfully",
"bucket_id": bucketID,
"assigned_user_id": req.AssignedUserID,
"assigned_tenant_id": req.AssignedTenantID,
})
}
// UnassignBucket removes assignment from a bucket
func (ba *BucketAssignments) UnassignBucket(w http.ResponseWriter, r *http.Request) {
// Check permissions - only admins can unassign buckets
if !ba.checkPermission(r, schema.PermissionWriteBuckets) {
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
return
}
vars := mux.Vars(r)
bucketID := vars["bucketId"]
// Remove assignment (simulate for now)
utils.ResponseSuccess(w, map[string]interface{}{
"message": "Bucket assignment removed successfully",
"bucket_id": bucketID,
})
}
// ListUserBuckets returns buckets assigned to a specific user
func (ba *BucketAssignments) ListUserBuckets(w http.ResponseWriter, r *http.Request) {
// Check permissions
if !ba.checkPermission(r, schema.PermissionReadBuckets) {
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
return
}
vars := mux.Vars(r)
userID := vars["userId"]
// Validate user exists
user, err := utils.DB.GetUser(userID)
if err != nil {
utils.ResponseErrorStatus(w, fmt.Errorf("user not found"), http.StatusNotFound)
return
}
// Get all buckets and filter by assignment
// For now, return empty list since we're not storing assignments yet
response := map[string]interface{}{
"user_id": userID,
"username": user.Username,
"buckets": []interface{}{},
}
utils.ResponseSuccess(w, response)
}
// ListTenantBuckets returns buckets assigned to a specific tenant
func (ba *BucketAssignments) ListTenantBuckets(w http.ResponseWriter, r *http.Request) {
// Check permissions
if !ba.checkPermission(r, schema.PermissionReadBuckets) {
utils.ResponseErrorStatus(w, nil, http.StatusForbidden)
return
}
vars := mux.Vars(r)
tenantID := vars["tenantId"]
// Validate tenant exists
tenant, err := utils.DB.GetTenant(tenantID)
if err != nil {
utils.ResponseErrorStatus(w, fmt.Errorf("tenant not found"), http.StatusNotFound)
return
}
// Get all buckets and filter by assignment
// For now, return empty list since we're not storing assignments yet
response := map[string]interface{}{
"tenant_id": tenantID,
"tenant_name": tenant.Name,
"buckets": []interface{}{},
}
utils.ResponseSuccess(w, response)
}
// Helper functions
// getBucketDisplayName returns the best display name for a bucket
func getBucketDisplayName(bucket *schema.Bucket) string {
if len(bucket.GlobalAliases) > 0 {
return bucket.GlobalAliases[0]
}
if len(bucket.LocalAliases) > 0 {
return bucket.LocalAliases[0].Alias
}
return bucket.ID
}
// checkPermission checks if user has required permission
func (ba *BucketAssignments) checkPermission(r *http.Request, permission schema.Permission) bool {
userID := utils.Session.Get(r, "user_id")
if userID == nil {
return false
}
user, err := utils.DB.GetUser(userID.(string))
if err != nil {
return false
}
return user.HasPermission(permission)
}

View File

@ -0,0 +1,264 @@
import { useState } from "react";
import {
Button,
Card,
Loading,
Modal,
Select,
Alert,
Badge,
} from "react-daisyui";
import {
useBucketAssignment,
useAssignBucket,
useUnassignBucket,
} from "@/hooks/useBucketAssignments";
import { useUsers, useTenants } from "@/hooks/useAdmin";
import { UserPlus, Building2, X, CheckCircle, AlertCircle } from "lucide-react";
interface Props {
bucketId: string;
bucketName: string;
isOpen: boolean;
onClose: () => void;
}
export default function BucketAssignmentManager({
bucketId,
bucketName,
isOpen,
onClose,
}: Props) {
const [assignmentType, setAssignmentType] = useState<"user" | "tenant" | "">("");
const [selectedUserId, setSelectedUserId] = useState("");
const [selectedTenantId, setSelectedTenantId] = useState("");
// Hooks
const { data: assignment, isLoading } = useBucketAssignment(bucketId);
const { data: users } = useUsers();
const { data: tenants } = useTenants();
const assignBucket = useAssignBucket();
const unassignBucket = useUnassignBucket();
const handleAssign = async () => {
const request = {
bucket_id: bucketId,
assigned_user_id: assignmentType === "user" ? selectedUserId : undefined,
assigned_tenant_id: assignmentType === "tenant" ? selectedTenantId : undefined,
};
try {
await assignBucket.mutateAsync(request);
onClose();
} catch (error) {
console.error("Error assigning bucket:", error);
}
};
const handleUnassign = async () => {
if (window.confirm("¿Estás seguro de que quieres remover la asignación de este bucket?")) {
try {
await unassignBucket.mutateAsync(bucketId);
onClose();
} catch (error) {
console.error("Error unassigning bucket:", error);
}
}
};
if (isLoading) {
return (
<Modal open={isOpen} onClickBackdrop={onClose}>
<Modal.Header>
<h3>Asignación de Bucket</h3>
</Modal.Header>
<Modal.Body className="text-center py-8">
<Loading />
</Modal.Body>
</Modal>
);
}
const isAssigned = assignment?.assigned_user_id || assignment?.assigned_tenant_id;
return (
<Modal open={isOpen} onClickBackdrop={onClose} className="w-11/12 max-w-2xl">
<Modal.Header>
<h3 className="flex items-center gap-2">
<UserPlus size={20} />
Asignación de Bucket: {bucketName}
</h3>
</Modal.Header>
<Modal.Body className="space-y-6">
{/* Current Assignment Status */}
<Card className="bg-base-100 p-4">
<h4 className="font-semibold mb-3">Estado Actual</h4>
{isAssigned ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{assignment?.assigned_user && (
<>
<UserPlus size={16} />
<span>Asignado a Usuario:</span>
<Badge color="primary">{assignment.assigned_user.username}</Badge>
</>
)}
{assignment?.assigned_tenant && (
<>
<Building2 size={16} />
<span>Asignado a Tenant:</span>
<Badge color="info">{assignment.assigned_tenant.name}</Badge>
</>
)}
</div>
<Button
size="sm"
variant="outline"
color="error"
onClick={handleUnassign}
loading={unassignBucket.isPending}
>
<X size={14} />
Remover
</Button>
</div>
<Alert status="success" className="text-sm">
<CheckCircle size={16} />
<div>
Este bucket está asignado y puede ser gestionado por el usuario/tenant especificado.
</div>
</Alert>
</div>
) : (
<Alert status="info" className="text-sm">
<AlertCircle size={16} />
<div>
Este bucket no está asignado a ningún usuario o tenant.
</div>
</Alert>
)}
</Card>
{/* Assignment Form */}
<Card className="bg-base-100 p-4">
<h4 className="font-semibold mb-3">
{isAssigned ? "Cambiar Asignación" : "Nueva Asignación"}
</h4>
<div className="space-y-4">
{/* Assignment Type Selection */}
<div>
<label className="label">
<span className="label-text">Tipo de Asignación</span>
</label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="assignment-type"
value="user"
checked={assignmentType === "user"}
onChange={(e) => setAssignmentType(e.target.value as "user")}
className="radio radio-primary"
/>
<UserPlus size={16} />
<span>Usuario</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="assignment-type"
value="tenant"
checked={assignmentType === "tenant"}
onChange={(e) => setAssignmentType(e.target.value as "tenant")}
className="radio radio-primary"
/>
<Building2 size={16} />
<span>Tenant</span>
</label>
</div>
</div>
{/* User Selection */}
{assignmentType === "user" && (
<div>
<label className="label">
<span className="label-text">Seleccionar Usuario</span>
</label>
<Select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="w-full"
>
<option value="">Seleccionar usuario...</option>
{users?.map((user) => (
<option key={user.id} value={user.id}>
{user.username} ({user.email}) - {user.role}
</option>
))}
</Select>
</div>
)}
{/* Tenant Selection */}
{assignmentType === "tenant" && (
<div>
<label className="label">
<span className="label-text">Seleccionar Tenant</span>
</label>
<Select
value={selectedTenantId}
onChange={(e) => setSelectedTenantId(e.target.value)}
className="w-full"
>
<option value="">Seleccionar tenant...</option>
{tenants?.map((tenant) => (
<option key={tenant.id} value={tenant.id}>
{tenant.name} - {tenant.description}
</option>
))}
</Select>
</div>
)}
{/* Help Text */}
{assignmentType && (
<div className="bg-base-200 p-3 rounded text-sm">
<p className="font-medium mb-1">¿Qué significa asignar un bucket?</p>
<ul className="list-disc list-inside space-y-1 text-xs">
<li>El usuario/tenant asignado puede gestionar las llaves de acceso del bucket</li>
<li>Puede modificar permisos y configuraciones específicas del bucket</li>
<li>Los administradores siempre mantienen acceso completo</li>
<li>Solo se puede asignar a un usuario O tenant, no ambos</li>
</ul>
</div>
)}
</div>
</Card>
</Modal.Body>
<Modal.Actions>
<Button onClick={onClose} variant="outline">
Cancelar
</Button>
{assignmentType && (
<Button
onClick={handleAssign}
loading={assignBucket.isPending}
disabled={
assignBucket.isPending ||
(assignmentType === "user" && !selectedUserId) ||
(assignmentType === "tenant" && !selectedTenantId)
}
color="primary"
>
{isAssigned ? "Cambiar Asignación" : "Asignar Bucket"}
</Button>
)}
</Modal.Actions>
</Modal>
);
}

View File

@ -0,0 +1,104 @@
import api from "@/lib/api";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
BucketAssignment,
AssignBucketRequest,
UserBucketsResponse,
TenantBucketsResponse,
} from "@/types/bucket-assignments";
import { toast } from "sonner";
// Get bucket assignment
export const useBucketAssignment = (bucketId: string) => {
return useQuery({
queryKey: ["bucket-assignment", bucketId],
queryFn: async () => {
const response = await api.get<BucketAssignment>(`/buckets/${bucketId}/assignment`);
return response?.data;
},
enabled: !!bucketId,
});
};
// Assign bucket to user/tenant
export const useAssignBucket = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: AssignBucketRequest) =>
api.put(`/buckets/${data.bucket_id}/assignment`, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["bucket-assignment", variables.bucket_id],
});
queryClient.invalidateQueries({
queryKey: ["buckets"],
});
if (variables.assigned_user_id) {
queryClient.invalidateQueries({
queryKey: ["user-buckets", variables.assigned_user_id],
});
}
if (variables.assigned_tenant_id) {
queryClient.invalidateQueries({
queryKey: ["tenant-buckets", variables.assigned_tenant_id],
});
}
toast.success("Bucket asignado exitosamente");
},
onError: (error: any) => {
toast.error(error.message || "Error al asignar bucket");
},
});
};
// Unassign bucket
export const useUnassignBucket = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (bucketId: string) => api.delete(`/buckets/${bucketId}/assignment`),
onSuccess: (_, bucketId) => {
queryClient.invalidateQueries({
queryKey: ["bucket-assignment", bucketId],
});
queryClient.invalidateQueries({
queryKey: ["buckets"],
});
queryClient.invalidateQueries({
queryKey: ["user-buckets"],
});
queryClient.invalidateQueries({
queryKey: ["tenant-buckets"],
});
toast.success("Asignación de bucket removida exitosamente");
},
onError: (error: any) => {
toast.error(error.message || "Error al remover asignación de bucket");
},
});
};
// Get buckets assigned to a user
export const useUserBuckets = (userId: string) => {
return useQuery({
queryKey: ["user-buckets", userId],
queryFn: async () => {
const response = await api.get<UserBucketsResponse>(`/users/${userId}/buckets`);
return response?.data;
},
enabled: !!userId,
});
};
// Get buckets assigned to a tenant
export const useTenantBuckets = (tenantId: string) => {
return useQuery({
queryKey: ["tenant-buckets", tenantId],
queryFn: async () => {
const response = await api.get<TenantBucketsResponse>(`/tenants/${tenantId}/buckets`);
return response?.data;
},
enabled: !!tenantId,
});
};

View File

@ -30,7 +30,11 @@ const BucketsPage = () => {
);
}
buckets = buckets.sort((a, b) => a.aliases[0].localeCompare(b.aliases[0]));
buckets = buckets.sort((a, b) => {
const aliasA = (a.aliases && a.aliases.length > 0) ? a.aliases[0] : a.id;
const aliasB = (b.aliases && b.aliases.length > 0) ? b.aliases[0] : b.id;
return aliasA.localeCompare(aliasB);
});
return buckets;
}, [data, search]);

View File

@ -0,0 +1,28 @@
import { User, Tenant } from "@/types/admin";
export interface BucketAssignment {
bucket_id: string;
bucket_name: string;
assigned_user_id?: string;
assigned_tenant_id?: string;
assigned_user?: User;
assigned_tenant?: Tenant;
}
export interface AssignBucketRequest {
bucket_id: string;
assigned_user_id?: string;
assigned_tenant_id?: string;
}
export interface UserBucketsResponse {
user_id: string;
username: string;
buckets: BucketAssignment[];
}
export interface TenantBucketsResponse {
tenant_id: string;
tenant_name: string;
buckets: BucketAssignment[];
}