Skip to content

Commit 457e2df

Browse files
feat: share resource
1 parent 324b15e commit 457e2df

58 files changed

Lines changed: 19292 additions & 4244 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cssconfig.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^8.0.0/components/context.jsonld",
3+
"import": [
4+
"css:config/app/init/default.json",
5+
"css:config/app/main/default.json",
6+
"css:config/app/variables/default.json",
7+
"css:config/http/handler/default.json",
8+
"css:config/http/middleware/default.json",
9+
"css:config/http/notifications/all.json",
10+
"css:config/http/server-factory/http.json",
11+
"css:config/http/static/default.json",
12+
"css:config/identity/access/public.json",
13+
"css:config/identity/email/default.json",
14+
"css:config/identity/handler/no-accounts.json",
15+
"css:config/identity/oidc/default.json",
16+
"css:config/identity/ownership/token.json",
17+
"css:config/identity/pod/static.json",
18+
"css:config/ldp/authentication/dpop-bearer.json",
19+
"css:config/ldp/authorization/acp.json",
20+
"css:config/ldp/handler/default.json",
21+
"css:config/ldp/metadata-parser/default.json",
22+
"css:config/ldp/metadata-writer/default.json",
23+
"css:config/ldp/modes/default.json",
24+
"css:config/storage/backend/file.json",
25+
"css:config/storage/key-value/memory.json",
26+
"css:config/storage/location/root.json",
27+
"css:config/storage/middleware/default.json",
28+
"css:config/util/auxiliary/acr.json",
29+
"css:config/util/identifiers/suffix.json",
30+
"css:config/util/index/default.json",
31+
"css:config/util/logging/winston.json",
32+
"css:config/util/representation-conversion/default.json",
33+
"css:config/util/resource-locker/memory.json",
34+
"css:config/util/variables/default.json"
35+
]
36+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ yarn-error.log*
3333
# env files (can opt-in for committing if needed)
3434
.env*
3535

36+
# seed config (contains passwords)
37+
seed-config.json
38+
src/ldo/
39+
3640
# vercel
3741
.vercel
3842

README-CSS.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Running Local Community Solid Server with ACP
2+
3+
## Setup
4+
5+
The project is configured to run a local Community Solid Server (CSS) with ACP (Access Control Policy) enabled.
6+
7+
## Configuration
8+
9+
- **Config file**: `.cssconfig.json` - Has ACP enabled via `css:config/ldp/authorization/acp.json`
10+
- **Seed config**: `seed-config.json` - Contains your account credentials (create from `seed-config.json.example`)
11+
- **Data directory**: `data/` - Where the server stores pod data
12+
13+
### Setting up your account
14+
15+
1. Copy the example seed config:
16+
```bash
17+
cp seed-config.json.example seed-config.json
18+
```
19+
20+
2. Edit `seed-config.json` and replace with your real email, password, and pod name:
21+
```json
22+
[
23+
{
24+
"email": "your-email@example.com",
25+
"password": "your-password",
26+
"pods": [
27+
{ "name": "your-pod-name" }
28+
]
29+
}
30+
]
31+
```
32+
33+
**Note**: `seed-config.json` is in `.gitignore` to prevent committing your password.
34+
35+
## Running the Server
36+
37+
### Option 1: Run CSS only
38+
```bash
39+
npm run start:css
40+
```
41+
42+
This will start CSS on port **3000** with:
43+
- ACP authorization enabled
44+
- Seeded accounts (alice and bob)
45+
- Data stored in `data/` directory
46+
47+
### Option 2: Run CSS + Next.js together
48+
```bash
49+
npm run start:dev
50+
```
51+
52+
This runs both:
53+
- CSS on port **3000**
54+
- Next.js dev server on port **3001**
55+
56+
## Test Accounts
57+
58+
After seeding, you can log in with any of these accounts:
59+
60+
1. **Ruky** (your account):
61+
- Email: `rukyjacob@gmail.com`
62+
- Password: `Test123$`
63+
- Pod URL: `http://localhost:3000/ruky/`
64+
- WebID: `http://localhost:3000/ruky/profile/card#me`
65+
66+
2. **Alice**:
67+
- Email: `alice@example.com`
68+
- Password: `alice123`
69+
- Pod URL: `http://localhost:3000/alice/`
70+
- WebID: `http://localhost:3000/alice/profile/card#me`
71+
72+
3. **Bob**:
73+
- Email: `bob@example.com`
74+
- Password: `bob123`
75+
- Pod URL: `http://localhost:3000/bob/`
76+
- WebID: `http://localhost:3000/bob/profile/card#me`
77+
78+
4. **Charlie**:
79+
- Email: `charlie@example.com`
80+
- Password: `charlie123`
81+
- Pod URL: `http://localhost:3000/charlie/`
82+
- WebID: `http://localhost:3000/charlie/profile/card#me`
83+
84+
## Testing ACP Sharing
85+
86+
The seed config includes 4 test accounts (ruky, alice, bob, charlie) for testing sharing.
87+
88+
**Steps to test:**
89+
1. Start the server: `npm run start:css`
90+
2. In your Next.js app (port 3001), log in as **ruky** (`rukyjacob@gmail.com` / `Test123$`)
91+
3. Navigate to ruky's pod storage
92+
4. Create a folder or file
93+
5. Click "Share" on the resource
94+
6. Add a WebID to share with (e.g., `http://localhost:3000/alice/profile/card#me`)
95+
7. Select access level (Editor/Viewer) and click "Done"
96+
97+
**Quick WebIDs for sharing:**
98+
- Alice: `http://localhost:3000/alice/profile/card#me`
99+
- Bob: `http://localhost:3000/bob/profile/card#me`
100+
- Charlie: `http://localhost:3000/charlie/profile/card#me`
101+
102+
## Notes
103+
104+
- The server uses ACP (not WAC), so ACRs should be at `.acr` location
105+
- Make sure to use `http://localhost:3000` as the OIDC issuer in your app
106+
- The server will create pods automatically when accounts are seeded
107+

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ solid-file-manager/
147147
│ │ ├── RenameDialog.tsx # Dialog for renaming resources
148148
│ │ ├── MoveDialog.tsx # Dialog for moving files
149149
│ │ ├── PreviewModal.tsx # Modal for previewing files
150-
│ │ ├── PermissionsDialog.tsx # Sharing/permissions dialog
151150
│ │ ├── FileUploadHandler.tsx # File upload component
152151
│ │ ├── ProfileIcon.tsx # User profile icon and logout
153152
│ │ ├── LoginPage.tsx # Login page

app/components/FileManager.tsx

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import Header from "./Header";
1919
import Sidebar from "./Sidebar";
2020
import Breadcrumb from "./Breadcrumb";
2121
import FileList from "./FileList";
22-
import PermissionsDialog, { Permission } from "./PermissionsDialog";
2322
import NewFolderDialog from "./NewFolderDialog";
2423
import RenameDialog from "./RenameDialog";
2524
import PreviewModal from "./PreviewModal";
2625
import MoveDialog from "./MoveDialog";
2726
import DeleteConfirmDialog from "./DeleteConfirmDialog";
2827
import ShareDialog, { AccessLevel } from "./ShareDialog";
28+
import ShareSuccessModal from "./ShareSuccessModal";
2929
import FileUploadHandler from "./FileUploadHandler";
3030
import ContextMenu, { ContextMenuAction } from "./ContextMenu";
3131
import { FileItemData } from "./FileItem";
@@ -48,6 +48,7 @@ import {
4848
hasFiles as hasFilesInDrag,
4949
isUnsupportedFolderDrag,
5050
} from "../lib/helpers";
51+
import { shareResourceWithAcp } from "../lib/helpers/acpUtils";
5152
import {
5253
getUrlFromSearchParams,
5354
getUrlFromStorage,
@@ -76,10 +77,6 @@ export default function FileManager() {
7677
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
7778
const [isInitialized, setIsInitialized] = useState(false);
7879
const [refreshKey, setRefreshKey] = useState(0);
79-
const [permissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
80-
const [selectedFileForPermissions, setSelectedFileForPermissions] =
81-
useState<FileItemData | null>(null);
82-
const [permissions, setPermissions] = useState<Permission[]>([]);
8380
const [sidebarOpen, setSidebarOpen] = useState(false);
8481
const [showNewFolderDialog, setShowNewFolderDialog] = useState(false);
8582
const [fileUploadTrigger, setFileUploadTrigger] = useState(0);
@@ -99,6 +96,9 @@ export default function FileManager() {
9996
const [showShareDialog, setShowShareDialog] = useState(false);
10097
const [fileToShare, setFileToShare] = useState<FileItemData | null>(null);
10198
const [contextMenuState, setContextMenuState] = useState<ContextMenuState | null>(null);
99+
const [showShareSuccessModal, setShowShareSuccessModal] = useState(false);
100+
const [sharedResourceUrl, setSharedResourceUrl] = useState<string>("");
101+
const [sharedResourceName, setSharedResourceName] = useState<string>("");
102102

103103
const closeContextMenu = () => setContextMenuState(null);
104104

@@ -401,10 +401,38 @@ export default function FileManager() {
401401
setShowShareDialog(true);
402402
};
403403

404-
const handleShareConfirm = async (webId: string, accessLevel: AccessLevel) => {
405-
// TODO: Implement actual sharing logic
406-
console.log(`Sharing ${fileToShare?.name} with ${webId} as ${accessLevel}`);
407-
toast.success(`Shared with ${webId} as ${accessLevel}`);
404+
const handleShareConfirm = async (webIds: string[], accessLevel: AccessLevel) => {
405+
if (!fileToShare) {
406+
return;
407+
}
408+
409+
const toastId = toast.loading(`Sharing "${fileToShare.name}"...`);
410+
try {
411+
// Get the resource URL - ensure it has a trailing slash for containers
412+
let resourceUrl = fileToShare.url;
413+
if (fileToShare.type === "folder" && !resourceUrl.endsWith("/")) {
414+
resourceUrl += "/";
415+
}
416+
417+
await shareResourceWithAcp(resourceUrl, webIds, accessLevel);
418+
419+
toast.success(`Successfully shared "${fileToShare.name}"`, { id: toastId });
420+
setShowShareDialog(false);
421+
422+
// Show success modal with resource URL
423+
setSharedResourceUrl(resourceUrl);
424+
setSharedResourceName(fileToShare.name);
425+
setShowShareSuccessModal(true);
426+
setFileToShare(null);
427+
} catch (error) {
428+
console.error("Failed to share resource:", error);
429+
toast.error(
430+
error instanceof Error
431+
? `Failed to share: ${error.message}`
432+
: "Failed to share resource",
433+
{ id: toastId }
434+
);
435+
}
408436
};
409437

410438
const handleDragEnter = (event: React.DragEvent<HTMLElement>) => {
@@ -711,41 +739,6 @@ export default function FileManager() {
711739
}
712740
};
713741

714-
const handleShareClickForFile = (file: FileItemData) => {
715-
setSelectedFileForPermissions(file);
716-
setPermissionsDialogOpen(true);
717-
setPermissions([
718-
{
719-
id: "1",
720-
type: "user",
721-
webId: "https://id.inrupt.com/user",
722-
name: "You",
723-
role: "owner",
724-
},
725-
]);
726-
};
727-
728-
const handleAddPermission = async (webId: string, role: "viewer" | "editor") => {
729-
const newPermission: Permission = {
730-
id: Date.now().toString(),
731-
type: "user",
732-
webId,
733-
name: webId.split("/").pop() || webId,
734-
role,
735-
};
736-
setPermissions((prev) => [...prev, newPermission]);
737-
};
738-
739-
const handleRemovePermission = (permissionId: string) => {
740-
setPermissions((prev) => prev.filter((p) => p.id !== permissionId));
741-
};
742-
743-
const handleUpdatePermission = (permissionId: string, role: "viewer" | "editor") => {
744-
setPermissions((prev) =>
745-
prev.map((p) => (p.id === permissionId ? { ...p, role } : p))
746-
);
747-
};
748-
749742
if (isLoadingStorages) {
750743
return (
751744
<AuthWrapper>
@@ -831,7 +824,7 @@ export default function FileManager() {
831824
</div>
832825
{isBrowsing ? (
833826
<div className="flex flex-1 items-center justify-center">
834-
<LoadingSpinner size="md" text="Loading folder contents..." />
827+
<LoadingSpinner size="md" text="Loading folder contents..." />
835828
</div>
836829
) : (
837830
<div className="flex-1 min-h-0 overflow-hidden">
@@ -854,20 +847,7 @@ export default function FileManager() {
854847
)}
855848
</main>
856849
</div>
857-
{selectedFileForPermissions && (
858-
<PermissionsDialog
859-
isOpen={permissionsDialogOpen}
860-
onClose={() => {
861-
setPermissionsDialogOpen(false);
862-
setSelectedFileForPermissions(null);
863-
}}
864-
fileName={selectedFileForPermissions.name}
865-
permissions={permissions}
866-
onAddPermission={handleAddPermission}
867-
onRemovePermission={handleRemovePermission}
868-
onUpdatePermission={handleUpdatePermission}
869-
/>
870-
)}
850+
871851
<NewFolderDialog
872852
isOpen={showNewFolderDialog}
873853
onClose={() => setShowNewFolderDialog(false)}
@@ -921,6 +901,16 @@ export default function FileManager() {
921901
file={fileToShare}
922902
onShare={handleShareConfirm}
923903
/>
904+
<ShareSuccessModal
905+
isOpen={showShareSuccessModal}
906+
onClose={() => setShowShareSuccessModal(false)}
907+
resourceUrl={sharedResourceUrl}
908+
resourceName={sharedResourceName}
909+
onOpenInApp={(url) => {
910+
// Navigate to the resource URL in the file manager
911+
updateUrl(url);
912+
}}
913+
/>
924914
{isDragActive && (
925915
<div className="pointer-events-none fixed inset-0 z-40 flex flex-col items-center justify-center bg-purple-500/10">
926916
<div className="rounded-2xl border border-purple-400 bg-white/90 px-8 py-6 text-center shadow-lg">

app/components/LoginPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Button from "./shared/Button";
88
const OIDC_ISSUERS = [
99
{ label: "Solid Community", value: "https://solidcommunity.net/" },
1010
{ label: "Inrupt", value: "https://login.inrupt.com" },
11+
{ label: "Local CSS (ACP)", value: "http://localhost:3000/" },
1112
] as const;
1213

1314
export default function LoginPage() {

0 commit comments

Comments
 (0)