Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -201,7 +201,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) {
return res.status(404).json({ error: "No avatar set" });
}
Expand All @@ -225,7 +225,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) {
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 @@ -74,8 +75,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 Down Expand Up @@ -104,10 +117,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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import type {
const PROFESSIONAL_PROFILE_ONTOLOGY = "550e8400-e29b-41d4-a716-446655440009";
const USER_ONTOLOGY = "550e8400-e29b-41d4-a716-446655440000";

function getFileManagerPublicUrl(fileId: string): string {
const base = process.env.PUBLIC_FILE_MANAGER_BASE_URL || "http://localhost:3005";
return `${base}/api/public/files/${fileId}`;
}

/** Match BindingDocumentService's normalizeSubject format for ACL entries */
function normalizeEName(eName: string): string {
return eName.startsWith("@") ? eName : `@${eName}`;
Expand Down Expand Up @@ -146,6 +151,12 @@ export class EVaultProfileService {
const userData = (userNode?.parsed ?? {}) as UserOntologyData;
const profData = (professionalNode?.parsed ?? {}) as ProfessionalProfile;

// Avatar/banner live on the local User entity (file-manager IDs),
// not in any eVault ontology.
const { AppDataSource } = await import("../database/data-source");
const { User } = await import("../database/entities/User");
const localUser = await AppDataSource.getRepository(User).findOneBy({ ename: eName });

const name =
profData.displayName ?? userData.displayName ?? eName;

Expand All @@ -158,8 +169,8 @@ export class EVaultProfileService {
displayName: profData.displayName,
headline: profData.headline,
bio: profData.bio,
avatarFileId: profData.avatarFileId,
bannerFileId: profData.bannerFileId,
avatar: localUser?.avatar ?? undefined,
banner: localUser?.banner ?? undefined,
cvFileId: profData.cvFileId,
videoIntroFileId: profData.videoIntroFileId,
email: profData.email,
Expand Down Expand Up @@ -268,9 +279,70 @@ export class EVaultProfileService {
}
}

// Always persist avatar/banner to the local User row first so
// getProfile returns the correct value immediately.
if (data.avatar !== undefined || data.banner !== undefined) {
const { AppDataSource } = await import("../database/data-source");
const { User } = await import("../database/entities/User");
const userRepo = AppDataSource.getRepository(User);
let localUser = await userRepo.findOneBy({ ename: eName });
if (!localUser) {
localUser = userRepo.create({ ename: eName });
}
if (data.avatar !== undefined) localUser.avatar = data.avatar;
if (data.banner !== undefined) localUser.banner = data.banner;
await userRepo.save(localUser);

// Propagate a public file-manager URL to the User ontology so
// other platforms (Pictique, Blabsy, etc.) pick it up.
this.syncAvatarBannerToUserOntology(client, eName, merged).catch(
(e) => console.error("Failed to sync avatar/banner to User ontology:", e),
);
}

return this.getProfile(eName);
}

/**
* Writes avatarUrl / bannerUrl as public file-manager URLs into the
* User ontology MetaEnvelope so other platforms can render them directly.
* Also updates the local User entity so the DB stays in sync.
*/
private async syncAvatarBannerToUserOntology(
client: GraphQLClient,
eName: string,
profile: ProfessionalProfile,
): Promise<void> {
try {
const userNode = await this.findMetaEnvelopeByOntology(client, USER_ONTOLOGY);
const existing = (userNode?.parsed ?? {}) as Record<string, unknown>;

const patch: Record<string, unknown> = { ...existing };
if (profile.avatar) {
patch.avatarUrl = getFileManagerPublicUrl(profile.avatar);
}
if (profile.banner) {
patch.bannerUrl = getFileManagerPublicUrl(profile.banner);
}

if (userNode) {
await client.request<UpdateResult>(UPDATE_MUTATION, {
id: userNode.id,
input: { ontology: USER_ONTOLOGY, payload: patch, acl: ["*"] },
});
Comment thread
sosweetham marked this conversation as resolved.
} else {
patch.ename = eName;
patch.displayName = profile.displayName ?? eName;
await client.request<CreateResult>(CREATE_MUTATION, {
input: { ontology: USER_ONTOLOGY, payload: patch, acl: ["*"] },
Comment thread
sosweetham marked this conversation as resolved.
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
sosweetham marked this conversation as resolved.

} catch (e) {
console.error("Failed to sync avatar/banner to User ontology:", e);
}
}

async getProfileByEnvelope(
eName: string,
id: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export class EVaultSyncService {
bio: profile.professional.bio,
headline: profile.professional.headline,
location: profile.professional.location,
avatarFileId: profile.professional.avatarFileId,
bannerFileId: profile.professional.bannerFileId,
avatar: profile.professional.avatar,
banner: profile.professional.banner,
skills: profile.professional.skills,
isPublic: profile.professional.isPublic === true,
isArchived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class UserSearchService {
"user.name",
"user.handle",
"user.bio",
"user.avatarFileId",
"user.avatar",
"user.headline",
"user.location",
"user.skills",
Expand Down Expand Up @@ -101,7 +101,7 @@ export class UserSearchService {
name: user.name,
handle: user.handle,
bio: user.bio,
avatarFileId: user.avatarFileId,
avatar: user.avatar,
headline: user.headline,
location: user.location,
skills: user.skills,
Expand Down
Loading