Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/src/internal/middleware/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package middleware

import (
"backend/src/internal/auth"
"context"
"net/http"
"strings"
)
Expand All @@ -13,7 +14,7 @@ func AuthMiddleware(a auth.Authenticator) func(http.Handler) http.Handler {
panic("not implemented")
}

func Protect(a *auth.Authenticator, next http.Handler) http.Handler {
func Protect(ctx context.Context, a *auth.Authenticator, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
if !strings.HasPrefix(h, "Bearer ") {
Expand All @@ -22,13 +23,13 @@ func Protect(a *auth.Authenticator, next http.Handler) http.Handler {
}

token := strings.TrimPrefix(h, "Bearer ")
ok, err := a.ValidateJWT(token)
newCtx, ok, err := a.ValidateJWT(ctx, token)
if err != nil || !ok {
http.Error(w, "unauthorized, invalid token", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
next.ServeHTTP(w, r.WithContext(newCtx))
})

}
52 changes: 40 additions & 12 deletions backend/src/usecase/files/service/service.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package files

import (
"backend/src/internal/auth"
data "backend/src/usecase/files/data"
repository "backend/src/usecase/files/repository"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"io"
"log"
"net/http"
Expand Down Expand Up @@ -43,10 +45,10 @@ type multipartMetadata struct {
CheckSum []byte `json:"checkSum"`
}

func (svc *Service) Upload(r *http.Request, ctx context.Context) error {
func (svc *Service) Upload(r *http.Request, ctx context.Context) ([]data.MetaData, error) {
mr, err := r.MultipartReader()
if err != nil {
return err
return nil, err
}

metadataByID := make(map[string]data.MetaData)
Expand All @@ -57,7 +59,7 @@ func (svc *Service) Upload(r *http.Request, ctx context.Context) error {
break
}
if err != nil {
return err
return nil, err
}

name := part.FormName()
Expand All @@ -70,12 +72,16 @@ func (svc *Service) Upload(r *http.Request, ctx context.Context) error {
// decode + build metadata
var decodedRequest multipartMetadata
if err := json.NewDecoder(part).Decode(&decodedRequest); err != nil {
return err
return nil, err
}

ownerID, err := uuid.Parse(decodedRequest.OwnerID)
userId, ok := auth.UserIDFromCtx(r.Context())
if !ok {
return nil, errors.New("could not get userID from context")
}
ownerID, err := uuid.Parse(userId)
if err != nil {
return err
return nil, err
}

metadataByID[idStr] = data.MetaData{
Expand All @@ -102,21 +108,24 @@ func (svc *Service) Upload(r *http.Request, ctx context.Context) error {
io.TeeReader(part, hash),
part.FileName(),
); err != nil {
return err
return nil, err
}
md := metadataByID[idStr]
md.CheckSum = hash.Sum(nil)
metadataByID[idStr] = md
}
}
// Persist file metadata
var newMetadata []data.MetaData
for _, md := range metadataByID {
if err := svc.repo.SaveMetaData(md, ctx); err != nil {
return err
newMd, err := svc.repo.SaveMetaData(md, ctx)
if err != nil {
return nil, err
}
newMetadata = append(newMetadata, newMd)
}

return nil
return newMetadata, nil
}

func (svc *Service) GetAll(ctx context.Context, request data.GetAllMetadataRequest) ([]data.MetaDataResponse, error) {
Expand Down Expand Up @@ -159,8 +168,17 @@ func (svc *Service) FindMetadata(ctx context.Context, request data.FindMetadataR
}

func (svc *Service) Delete(ctx context.Context, request data.DeleteRequest) error {
userID, ok := auth.UserIDFromCtx(ctx)
if !ok {
return errors.New("unable to get userID from context")
}

err := svc.repo.DeleteMetadata(ctx, request.ID, request.OwnerID)
ownerID, err := uuid.Parse(userID)
if err != nil {
log.Printf("unable to parse userID string to uuid")
}

err = svc.repo.DeleteMetadata(ctx, request.ID, ownerID)
if err != nil {
log.Printf("could not delete file metadata, %v", err)
return err
Expand All @@ -170,7 +188,17 @@ func (svc *Service) Delete(ctx context.Context, request data.DeleteRequest) erro
}

func (svc *Service) MoveToRubbish(ctx context.Context, request data.DeleteRequest) error {
err := svc.repo.MarkForDeletion(ctx, request.ID, request.OwnerID)
userID, ok := auth.UserIDFromCtx(ctx)
if !ok {
return errors.New("unable to get userID from context")
}

ownerID, err := uuid.Parse(userID)
if err != nil {
log.Printf("unable to parse userID string to uuid")
}

err = svc.repo.MarkForDeletion(ctx, request.ID, ownerID)
if err != nil {
log.Printf("unable to move file or metadata to rubbish bin, %v", err)
return err
Expand Down
30 changes: 0 additions & 30 deletions frontend/src/app/features/files/components/dialogs/file-dialog.tsx

This file was deleted.

This file was deleted.

69 changes: 69 additions & 0 deletions frontend/src/app/features/files/components/file-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger} from "@/components/ui/dialog.tsx";
import type {Metadata} from "@/app/features/files/types.ts";
import {useState} from "react";
import {Button} from "@/components/ui/button.tsx";
import {EllipsisVertical} from "lucide-react";
import {DialogDescription} from "@radix-ui/react-dialog";

interface FileDialogProps{
open: boolean,
onOpenChange: (open: boolean) => void,
metadata: Metadata,
ipfsLink: string,
spaceName: string,
spaceDid: string
}

export function FileDialog({
open,
onOpenChange,
metadata,
ipfsLink,
spaceName,
spaceDid,
}: FileDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{metadata.file_name}</DialogTitle>
<DialogDescription>
File info and backup uris
</DialogDescription>
</DialogHeader>

<h2>File size: {metadata.size / 1024} KB</h2>
<h2>Last modified at: {formatDate(metadata.modified_at)}</h2>
<h2>
Uploaded at:{" "}
{metadata.uploaded_at
? formatDate(metadata.uploaded_at)
: "11/02/2026 11:38:42"}
</h2>
<h2>{metadata.uuid}</h2>
<h2>{metadata.owner_id}</h2>

<h2>Backup</h2>
<h3>Space: test-space</h3>
<p>Visibility: PUBLIC</p>
<p className={"truncate"}>DID: bafybeia7wkemsgryogneimjafwwkb33ifwh2oo3djba3lqfeg3lkrqn464</p>
<p className={"truncate"}>Shards: bagbaierahrldusuunn3mt3xcgfue3aav6zcynk7ynpwzhgyi4l6muyp4hjhq</p>
<h1>Recovery </h1>
<Button variant={"outline"}>
<a href={"https://bafybeia7wkemsgryogneimjafwwkb33ifwh2oo3djba3lqfeg3lkrqn464.ipfs.w3s.link/"}> Fetch from IPFS</a>
</Button>
</DialogContent>
</Dialog>
);
}

//TODO: remove this duplcate function at some point
function formatDate(date: string): string {
const d = new Date(date);

if (isNaN(d.getTime())) {
return "11/02/2026 11:38AM";
}

return d.toLocaleString();
}
81 changes: 81 additions & 0 deletions frontend/src/app/features/files/components/file-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { EllipsisVertical, Info, Settings, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAuthStore } from "@/security/auth/authstore/auth-store";
import { RestHandler } from "@/app/features/shared/api/rest/rest-handler";

interface FileDropdownProps {
fileId: string;
onDeleted: () => void;
}

export function FileDropdown({ fileId, onDeleted }: FileDropdownProps) {
const userId = useAuthStore((s) => s.userId);

async function handleDelete(e: React.MouseEvent) {
e.stopPropagation();

if (!userId) {
console.error("No user ID found");
return;
}

try {
const api = new RestHandler("http://localhost:8081");

await api.handlePost<
{ id: string; },
void
>("api/files/delete", {
id: fileId,
});

onDeleted();

} catch (error) {
console.error("Failed to delete file on server:", error);
}
}

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ms-1 cursor-pointer"
>
<EllipsisVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent align="end">
<DropdownMenuItem
className="cursor-pointer text-destructive focus:text-destructive"
onClick={handleDelete}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>

<DropdownMenuSeparator />

<DropdownMenuItem className="cursor-pointer">
<Info className="mr-2 h-4 w-4" />
<span>File Info</span>
</DropdownMenuItem>

<DropdownMenuItem className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
<span>File Settings</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Loading
Loading