Skip to content

Commit 81ef468

Browse files
committed
fix: profile editor media sync
1 parent d22457f commit 81ef468

26 files changed

Lines changed: 464 additions & 53 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ PUBLIC_PROVISIONER_SHARED_SECRET="your-provisioner-shared-secret"
9292

9393
PUBLIC_ESIGNER_BASE_URL="http://localhost:3004"
9494
PUBLIC_FILE_MANAGER_BASE_URL="http://localhost:3005"
95-
PUBLIC_PROFILE_EDITOR_BASE_URL="http://localhost:3006"
95+
PUBLIC_PROFILE_EDITOR_BASE_URL="http://localhost:3007"
9696

9797
DREAMSYNC_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dreamsync
9898
VITE_DREAMSYNC_BASE_URL="http://localhost:8888"

infrastructure/dev-sandbox/src/routes/+page.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface Identity {
4949
w3id: string;
5050
uri: string;
5151
keyId: string;
52+
displayName?: string;
5253
bearerToken?: string;
5354
tokenExpiresAt?: number;
5455
}
@@ -468,6 +469,10 @@ async function doProvision() {
468469
identity.w3id,
469470
profile,
470471
);
472+
const next = [...identities];
473+
next[selectedIndex] = { ...next[selectedIndex], displayName: profile.displayName };
474+
identities = next;
475+
saveIdentities(next);
471476
addLog("success", "UserProfile created", profile.displayName);
472477
} catch (e) {
473478
const msg = e instanceof Error ? e.message : String(e);
@@ -826,7 +831,7 @@ async function doSign() {
826831
<h2>Selected identity</h2>
827832
<select bind:value={selectedIndex}>
828833
{#each identities as id, i}
829-
<option value={i}>{id.w3id}</option>
834+
<option value={i}>{id.displayName ? `${id.displayName} (${id.w3id})` : id.w3id}</option>
830835
{/each}
831836
</select>
832837
</section>

platforms/file-manager/api/src/controllers/FileController.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,38 @@ export class FileController {
567567
}
568568
};
569569

570+
/**
571+
* Serves a file publicly without authentication.
572+
* The file ID acts as an unguessable capability token.
573+
*/
574+
publicPreview = async (req: Request, res: Response) => {
575+
try {
576+
const { id } = req.params;
577+
const file = await this.fileService.getFileByIdPublic(id);
578+
579+
if (!file) {
580+
return res.status(404).json({ error: "File not found" });
581+
}
582+
583+
if (file.url) {
584+
return res.redirect(file.url);
585+
}
586+
587+
// Legacy fallback for files still in DB
588+
res.setHeader("Content-Type", file.mimeType);
589+
res.setHeader(
590+
"Content-Disposition",
591+
`inline; filename="${file.name}"`,
592+
);
593+
res.setHeader("Content-Length", file.size.toString());
594+
res.setHeader("Cache-Control", "public, max-age=3600");
595+
res.send(file.data);
596+
} catch (error) {
597+
console.error("Error serving public file:", error);
598+
res.status(500).json({ error: "Failed to serve file" });
599+
}
600+
};
601+
570602
deleteFile = async (req: Request, res: Response) => {
571603
try {
572604
if (!req.user) {

platforms/file-manager/api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ app.get("/api/auth/offer", authController.getOffer);
8080
app.post("/api/auth", authController.login);
8181
app.get("/api/auth/sessions/:id", authController.sseStream);
8282
app.post("/api/webhook", webhookController.handleWebhook);
83+
app.get("/api/public/files/:id", fileController.publicPreview);
8384

8485
// Protected routes (auth required)
8586
app.use(authMiddleware);

platforms/file-manager/api/src/services/FileService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ export class FileService {
114114
return savedFile;
115115
}
116116

117+
async getFileByIdPublic(id: string): Promise<File | null> {
118+
const file = await this.fileRepository.findOne({
119+
where: { id },
120+
});
121+
if (!file || file.name === SOFT_DELETED_FILE_NAME) {
122+
return null;
123+
}
124+
return file;
125+
}
126+
117127
async getFileById(id: string, userId: string): Promise<File | null> {
118128
const file = await this.fileRepository.findOne({
119129
where: { id },

platforms/marketplace/client/client/src/data/apps.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,13 @@
8787
"category": "Storage",
8888
"logoUrl": "/emover.png",
8989
"url": "https://emover.w3ds.metastate.foundation"
90+
},
91+
{
92+
"id": "profile-editor",
93+
"name": "Profile Editor",
94+
"description": "Create and manage your professional profile across the W3DS ecosystem. Showcase your skills, experience, and credentials.",
95+
"category": "Identity",
96+
"logoUrl": "/profile-editor.png",
97+
"url": "https://profile-editor.w3ds.metastate.foundation"
9098
}
9199
]

platforms/marketplace/client/client/src/pages/app-detail.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const appDetails: Record<string, { fullDescription: string; screenshots: string[
4646
"file-manager": {
4747
fullDescription: "File Manager is a decentralized file management system built on the Web 3.0 Data Space (W3DS) architecture. Organize, store, and share files with complete control over your data across the MetaState ecosystem.\n\nBuilt around the principle of data-platform separation, all your files are stored in your own sovereign eVault. Manage folders, organize documents, control access, and share files securely. Experience file management reimagined with privacy-first principles and complete data sovereignty.",
4848
screenshots: []
49+
},
50+
"profile-editor": {
51+
fullDescription: "Profile Editor is a professional profile management platform built on the Web 3.0 Data Space (W3DS) architecture. Create, edit, and share your professional profile across the entire MetaState ecosystem with a single source of truth.\n\nShowcase your work experience, education, skills, and social links. Upload your CV and video introduction. All your profile data is stored in your own sovereign eVault and automatically synced across every W3DS platform — update once, reflected everywhere.",
52+
screenshots: []
4953
}
5054
};
5155

platforms/pictique/api/src/controllers/WebhookController.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export class WebhookController {
4343
);
4444
this.adapter.addToLockedIds(globalId);
4545

46-
if (!mapping) throw new Error();
46+
if (!mapping) {
47+
return res.status(200).send();
48+
}
4749
const local = await this.adapter.fromGlobal({
4850
data: req.body.data,
4951
mapping,

platforms/profile-editor/api/src/controllers/ProfileController.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export class ProfileController {
201201
return res.status(403).json({ error: "This profile is private" });
202202
}
203203

204-
const fileId = profile.professional.avatarFileId;
204+
const fileId = profile.professional.avatar;
205205
if (!fileId) {
206206
return res.status(404).json({ error: "No avatar set" });
207207
}
@@ -225,7 +225,7 @@ export class ProfileController {
225225
return res.status(403).json({ error: "This profile is private" });
226226
}
227227

228-
const fileId = profile.professional.bannerFileId;
228+
const fileId = profile.professional.banner;
229229
if (!fileId) {
230230
return res.status(404).json({ error: "No banner set" });
231231
}

platforms/profile-editor/api/src/controllers/WebhookController.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Request, Response } from "express";
22
import { Web3Adapter } from "web3-adapter";
33
import { UserSearchService } from "../services/UserSearchService";
4+
import { downloadUrlAndUploadToFileManager } from "../utils/file-proxy";
45

56
export class WebhookController {
67
private userSearchService: UserSearchService;
@@ -74,8 +75,20 @@ export class WebhookController {
7475
isArchived: localData.isArchived ?? false,
7576
};
7677

77-
if (localData.avatarFileId) userData.avatarFileId = localData.avatarFileId;
78-
if (localData.bannerFileId) userData.bannerFileId = localData.bannerFileId;
78+
if (localData.avatar) userData.avatar = localData.avatar;
79+
if (localData.banner) userData.banner = localData.banner;
80+
81+
// If the source platform sent a URL (Blabsy/Pictique) instead of a
82+
// file-manager ID, download the image and upload it to file-manager.
83+
if (!userData.avatar && rawBody.data?.avatarUrl) {
84+
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.avatarUrl, ename);
85+
if (fileId) userData.avatar = fileId;
86+
}
87+
if (!userData.banner && rawBody.data?.bannerUrl) {
88+
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.bannerUrl, ename);
89+
if (fileId) userData.banner = fileId;
90+
}
91+
7992
if (localData.location) userData.location = localData.location;
8093

8194
const user = await this.userSearchService.upsertFromWebhook(userData);
@@ -104,10 +117,22 @@ export class WebhookController {
104117
}
105118
if (localData.headline) profileData.headline = localData.headline;
106119
if (localData.bio) profileData.bio = localData.bio;
107-
if (localData.avatarFileId)
108-
profileData.avatarFileId = localData.avatarFileId;
109-
if (localData.bannerFileId)
110-
profileData.bannerFileId = localData.bannerFileId;
120+
if (localData.avatar)
121+
profileData.avatar = localData.avatar;
122+
if (localData.banner)
123+
profileData.banner = localData.banner;
124+
125+
// If the source platform sent a URL instead of a file-manager ID,
126+
// download the image and upload it to file-manager.
127+
if (!profileData.avatar && rawBody.data?.avatarUrl) {
128+
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.avatarUrl, ename);
129+
if (fileId) profileData.avatar = fileId;
130+
}
131+
if (!profileData.banner && rawBody.data?.bannerUrl) {
132+
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.bannerUrl, ename);
133+
if (fileId) profileData.banner = fileId;
134+
}
135+
111136
if (localData.cvFileId) profileData.cvFileId = localData.cvFileId;
112137
if (localData.videoIntroFileId)
113138
profileData.videoIntroFileId = localData.videoIntroFileId;

0 commit comments

Comments
 (0)