Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ PUBLIC_PROVISIONER_SHARED_SECRET="your-provisioner-shared-secret"

PUBLIC_ESIGNER_BASE_URL="http://localhost:3004"
PUBLIC_FILE_MANAGER_BASE_URL="http://localhost:3005"
PUBLIC_PROFILE_EDITOR_BASE_URL="http://localhost:3006"
PUBLIC_PROFILE_EDITOR_BASE_URL="http://localhost:3007"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

DREAMSYNC_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dreamsync
VITE_DREAMSYNC_BASE_URL="http://localhost:8888"
Expand Down
7 changes: 6 additions & 1 deletion infrastructure/dev-sandbox/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface Identity {
w3id: string;
uri: string;
keyId: string;
displayName?: string;
bearerToken?: string;
tokenExpiresAt?: number;
}
Expand Down Expand Up @@ -468,6 +469,10 @@ async function doProvision() {
identity.w3id,
profile,
);
const next = [...identities];
next[selectedIndex] = { ...next[selectedIndex], displayName: profile.displayName };
identities = next;
saveIdentities(next);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
addLog("success", "UserProfile created", profile.displayName);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
Expand Down Expand Up @@ -826,7 +831,7 @@ async function doSign() {
<h2>Selected identity</h2>
<select bind:value={selectedIndex}>
{#each identities as id, i}
<option value={i}>{id.w3id}</option>
<option value={i}>{id.displayName ? `${id.displayName} (${id.w3id})` : id.w3id}</option>
{/each}
</select>
</section>
Expand Down
32 changes: 32 additions & 0 deletions platforms/file-manager/api/src/controllers/FileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,38 @@ export class FileController {
}
};

/**
* Serves a file publicly without authentication.
* The file ID acts as an unguessable capability token.
*/
publicPreview = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const file = await this.fileService.getFileByIdPublic(id);
Comment thread
sosweetham marked this conversation as resolved.

if (!file) {
return res.status(404).json({ error: "File not found" });
}

if (file.url) {
return res.redirect(file.url);
}

// Legacy fallback for files still in DB
res.setHeader("Content-Type", file.mimeType);
res.setHeader(
"Content-Disposition",
`inline; filename="${file.name}"`,
);
res.setHeader("Content-Length", file.size.toString());
res.setHeader("Cache-Control", "public, max-age=3600");
res.send(file.data);
} catch (error) {
console.error("Error serving public file:", error);
res.status(500).json({ error: "Failed to serve file" });
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

deleteFile = async (req: Request, res: Response) => {
try {
if (!req.user) {
Expand Down
1 change: 1 addition & 0 deletions platforms/file-manager/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ app.get("/api/auth/offer", authController.getOffer);
app.post("/api/auth", authController.login);
app.get("/api/auth/sessions/:id", authController.sseStream);
app.post("/api/webhook", webhookController.handleWebhook);
app.get("/api/public/files/:id", fileController.publicPreview);
Comment thread
sosweetham marked this conversation as resolved.

// Protected routes (auth required)
app.use(authMiddleware);
Expand Down
10 changes: 10 additions & 0 deletions platforms/file-manager/api/src/services/FileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ export class FileService {
return savedFile;
}

async getFileByIdPublic(id: string): Promise<File | null> {
const file = await this.fileRepository.findOne({
where: { id },
});
if (!file || file.name === SOFT_DELETED_FILE_NAME) {
return null;
}
return file;
}
Comment thread
sosweetham marked this conversation as resolved.

async getFileById(id: string, userId: string): Promise<File | null> {
const file = await this.fileRepository.findOne({
where: { id },
Expand Down
8 changes: 8 additions & 0 deletions platforms/marketplace/client/client/src/data/apps.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,13 @@
"category": "Storage",
"logoUrl": "/emover.png",
"url": "https://emover.w3ds.metastate.foundation"
},
{
"id": "profile-editor",
"name": "Profile Editor",
"description": "Create and manage your professional profile across the W3DS ecosystem. Showcase your skills, experience, and credentials.",
"category": "Identity",
"logoUrl": "/profile-editor.png",
"url": "https://profile-editor.w3ds.metastate.foundation"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const appDetails: Record<string, { fullDescription: string; screenshots: string[
"file-manager": {
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.",
screenshots: []
},
"profile-editor": {
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.",
screenshots: []
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export class WebhookController {
);
this.adapter.addToLockedIds(globalId);

if (!mapping) throw new Error();
if (!mapping) {
return res.status(200).send();
}
const local = await this.adapter.fromGlobal({
data: req.body.data,
mapping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export class ProfileController {
data: Partial<ProfessionalProfile>,
res: Response,
) {
console.log(`[controller] optimisticUpdate ${ename}: keys=[${Object.keys(data).join(",")}] avatarFileId=${(data as any).avatarFileId ?? "N/A"} bannerFileId=${(data as any).bannerFileId ?? "N/A"}`);
console.log(`[controller] optimisticUpdate ${ename}: keys=[${Object.keys(data).join(",")}] avatar=${(data as any).avatar ?? "N/A"} banner=${(data as any).banner ?? "N/A"}`);
const { profile, persisted } = await this.evaultService.prepareUpdate(ename, data);
console.log(`[controller] optimisticUpdate ${ename}: returning avatarFileId=${profile.professional.avatarFileId ?? "NONE"} bannerFileId=${profile.professional.bannerFileId ?? "NONE"}`);
console.log(`[controller] optimisticUpdate ${ename}: returning avatar=${profile.professional.avatar ?? "NONE"} banner=${profile.professional.banner ?? "NONE"}`);
// Fire eVault write in background — don't block the response
persisted
.then(() => {
Expand Down Expand Up @@ -221,7 +221,7 @@ export class ProfileController {
return res.status(403).json({ error: "This profile is private" });
}

const fileId = profile.professional.avatarFileId;
const fileId = profile.professional.avatar;
if (!fileId) {
console.log(`[profile] avatar ${ename}: no fileId set, keys=[${Object.keys(profile.professional).join(",")}]`);
return res.status(404).json({ error: "No avatar set" });
Expand Down Expand Up @@ -249,7 +249,7 @@ export class ProfileController {
return res.status(403).json({ error: "This profile is private" });
}

const fileId = profile.professional.bannerFileId;
const fileId = profile.professional.banner;
if (!fileId) {
console.log(`[profile] banner ${ename}: no fileId set, keys=[${Object.keys(profile.professional).join(",")}]`);
return res.status(404).json({ error: "No banner set" });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response } from "express";
import { Web3Adapter } from "web3-adapter";
import { UserSearchService } from "../services/UserSearchService";
import { downloadUrlAndUploadToFileManager } from "../utils/file-proxy";

export class WebhookController {
private userSearchService: UserSearchService;
Expand Down Expand Up @@ -75,8 +76,20 @@ export class WebhookController {
isArchived: localData.isArchived ?? false,
};

if (localData.avatarFileId) userData.avatarFileId = localData.avatarFileId;
if (localData.bannerFileId) userData.bannerFileId = localData.bannerFileId;
if (localData.avatar) userData.avatar = localData.avatar;
if (localData.banner) userData.banner = localData.banner;

// If the source platform sent a URL (Blabsy/Pictique) instead of a
// file-manager ID, download the image and upload it to file-manager.
if (!userData.avatar && rawBody.data?.avatarUrl) {
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.avatarUrl, ename);
if (fileId) userData.avatar = fileId;
}
if (!userData.banner && rawBody.data?.bannerUrl) {
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.bannerUrl, ename);
if (fileId) userData.banner = fileId;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (localData.location) userData.location = localData.location;

const user = await this.userSearchService.upsertFromWebhook(userData);
Expand All @@ -98,7 +111,7 @@ export class WebhookController {
const ename = rawBody.w3id;
if (!ename) return;

console.log(`[webhook] professional_profile ${ename}: avatarFileId=${localData.avatarFileId ?? "NONE"} bannerFileId=${localData.bannerFileId ?? "NONE"} keys=[${Object.keys(localData).join(",")}]`);
console.log(`[webhook] professional_profile ${ename}: avatar=${localData.avatar ?? "NONE"} banner=${localData.banner ?? "NONE"} keys=[${Object.keys(localData).join(",")}]`);

const profileData: any = { ename };

Expand All @@ -107,10 +120,22 @@ export class WebhookController {
}
if (localData.headline) profileData.headline = localData.headline;
if (localData.bio) profileData.bio = localData.bio;
if (localData.avatarFileId)
profileData.avatarFileId = localData.avatarFileId;
if (localData.bannerFileId)
profileData.bannerFileId = localData.bannerFileId;
if (localData.avatar)
profileData.avatar = localData.avatar;
if (localData.banner)
profileData.banner = localData.banner;

// If the source platform sent a URL instead of a file-manager ID,
// download the image and upload it to file-manager.
if (!profileData.avatar && rawBody.data?.avatarUrl) {
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.avatarUrl, ename);
if (fileId) profileData.avatar = fileId;
}
if (!profileData.banner && rawBody.data?.bannerUrl) {
const fileId = await downloadUrlAndUploadToFileManager(rawBody.data.bannerUrl, ename);
if (fileId) profileData.banner = fileId;
}

if (localData.cvFileId) profileData.cvFileId = localData.cvFileId;
if (localData.videoIntroFileId)
profileData.videoIntroFileId = localData.videoIntroFileId;
Expand Down
4 changes: 2 additions & 2 deletions platforms/profile-editor/api/src/database/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export class User {
bio!: string;

@Column({ nullable: true })
avatarFileId!: string;
avatar!: string;

@Column({ nullable: true })
bannerFileId!: string;
banner!: string;

@Column({ nullable: true })
headline!: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class RenameAvatarBannerColumns1775600000000 implements MigrationInterface {
name = 'RenameAvatarBannerColumns1775600000000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "avatarFileId" TO "avatar"`);
await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "bannerFileId" TO "banner"`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "avatar" TO "avatarFileId"`);
await queryRunner.query(`ALTER TABLE "users" RENAME COLUMN "banner" TO "bannerFileId"`);
}
}
2 changes: 1 addition & 1 deletion platforms/profile-editor/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { EVaultSyncService } from "./services/EVaultSyncService";
import { adapter } from "./web3adapter/watchers/subscriber";

const app = express();
const PORT = process.env.PROFILE_EDITOR_API_PORT || 3006;
const PORT = process.env.PROFILE_EDITOR_API_PORT || 3007;

app.use(cors());
app.use(express.json());
Expand Down
Loading
Loading