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
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

DREAMSYNC_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dreamsync
VITE_DREAMSYNC_BASE_URL="http://localhost:8888"
Expand Down
11 changes: 10 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,14 @@ async function doProvision() {
identity.w3id,
profile,
);
const provisionedKeyId = identity.keyId;
const next = identities.map((id) =>
id.keyId === provisionedKeyId
? { ...id, displayName: profile.displayName }
: id,
);
identities = next;
saveIdentities(next);
addLog("success", "UserProfile created", profile.displayName);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
Expand Down Expand Up @@ -826,7 +835,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
43 changes: 42 additions & 1 deletion platforms/file-manager/api/src/controllers/FileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ export class FileController {
}

const { id } = req.params;
const { displayName, description, folderId } = req.body;
const { displayName, description, folderId, isPublic } = req.body;

const file = await this.fileService.updateFile(
id,
Expand All @@ -473,6 +473,11 @@ export class FileController {
.json({ error: "File not found or not authorized" });
}

if (typeof isPublic === "boolean") {
await this.fileService.setFilePublic(id, isPublic);
file.isPublic = isPublic;
}

res.json({
id: file.id,
name: file.name,
Expand All @@ -483,6 +488,7 @@ export class FileController {
md5Hash: file.md5Hash,
ownerId: file.ownerId,
folderId: file.folderId,
isPublic: file.isPublic,
createdAt: file.createdAt,
updatedAt: file.updatedAt,
});
Expand Down Expand Up @@ -567,6 +573,41 @@ export class FileController {
}
};

/**
* Serves a file publicly. Only files explicitly marked isPublic=true
* are served; all others return 404.
*/
publicPreview = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const file = await this.fileService.getFileByIdPublic(id);

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

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

if (!file.data) {
return res.status(410).json({ error: "File data unavailable" });
}

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" });
}
};

deleteFile = async (req: Request, res: Response) => {
try {
if (!req.user) {
Expand Down
3 changes: 3 additions & 0 deletions platforms/file-manager/api/src/database/entities/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export class File {
@Column({ type: "text", nullable: true })
url!: string | null;

@Column({ default: false })
isPublic!: boolean;

@Column()
ownerId!: string;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddIsPublicToFiles1775700000000 implements MigrationInterface {
name = 'AddIsPublicToFiles1775700000000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "files" ADD "isPublic" boolean NOT NULL DEFAULT false`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "files" DROP COLUMN "isPublic"`);
}
}
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);

// Protected routes (auth required)
app.use(authMiddleware);
Expand Down
14 changes: 14 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,20 @@ export class FileService {
return savedFile;
}

async getFileByIdPublic(id: string): Promise<File | null> {
const file = await this.fileRepository.findOne({
where: { id, isPublic: true },
});
if (!file || file.name === SOFT_DELETED_FILE_NAME) {
return null;
}
return file;
}

async setFilePublic(id: string, isPublic: boolean): Promise<void> {
await this.fileRepository.update(id, { isPublic });
}

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
8 changes: 6 additions & 2 deletions platforms/pictique/api/src/controllers/WebhookController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ export class WebhookController {
const mapping = Object.values(this.adapter.mapping).find(
(m) => m.schemaId === schemaId
);
this.adapter.addToLockedIds(globalId);

if (!mapping) throw new Error();
if (!mapping) {
console.log(`[webhook] skipping unknown schema ${schemaId} for ${globalId}`);
return res.status(200).send();
}

this.adapter.addToLockedIds(globalId);
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;
}

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