diff --git a/README.md b/README.md index fc6160dc..65da5ad7 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ The application is fully deployed and accessible online: blockchain architecture, smart contracts, and full Web3 applications. 4. **Hackathon Project Idea Generator**: Overcome coder's block by generating ideas based on technology and sector preferences. -5. **Open Source Contribution Trainer**: Get hands-on with Git, simulated GitHub issues, and PR - exercises to confidently contribute to open source. +5. **Open Source Contribution Trainer**: Get hands-on with Git, simulated GitHub issues, PR + exercises, and decentralized identity verification that attaches DID-backed contributor proof to + saved training submissions. ## 🛠 Technology Stack @@ -70,6 +71,18 @@ web3-student-lab/ └── docs/ # Documentation and learning materials ``` +## 🔐 MVP Update: Decentralized Identity Verification + +The Open Source Contribution Trainer now includes decentralized identity verification for contributor +workflows in `frontend/src/app/version-control/page.tsx`. + +- Contributors link a DID, Stellar wallet address, and GitHub handle before saving a verified + trainer version. +- Verified saves persist proof metadata in the version history engine at + `frontend/src/lib/version-control/engine.ts`. +- Core attestation creation and verification logic lives in + `frontend/src/lib/open-source-trainer/identity.ts`. + ## 🤝 Contributing We love our contributors! This project is being built for students, by students and open-source diff --git a/frontend/src/app/version-control/page.tsx b/frontend/src/app/version-control/page.tsx index 82877044..0a94e307 100644 --- a/frontend/src/app/version-control/page.tsx +++ b/frontend/src/app/version-control/page.tsx @@ -1,55 +1,189 @@ -'use client'; +"use client"; -import { useState, useCallback, useMemo } from 'react'; -import { VersionControl, type DocumentEntry, type Version } from '@/lib/version-control/engine'; -import { VersionHistory } from '@/components/version-control/VersionHistory'; -import { Plus, FileText, Trash2, Clock, RotateCcw } from 'lucide-react'; -import { formatDistanceToNow } from '@/lib/utils'; +import { useState, useCallback, useMemo, useEffect } from "react"; +import { + VersionControl, + type DocumentEntry, + type Version, + type VerifiedIdentityMetadata, +} from "@/lib/version-control/engine"; +import { VersionHistory } from "@/components/version-control/VersionHistory"; +import { + IdentityVerificationStore, + createIdentityAttestation, + verifyIdentityAttestation, + type DecentralizedIdentityProfile, + type IdentityVerificationAttestation, + type IdentityVerificationResult, +} from "@/lib/open-source-trainer/identity"; +import { + Plus, + FileText, + Trash2, + Clock, + RotateCcw, + ShieldCheck, + ShieldAlert, + BadgeCheck, + LoaderCircle, +} from "lucide-react"; +import { formatDistanceToNow } from "@/lib/utils"; + +const DEFAULT_IDENTITY_PROFILE: DecentralizedIdentityProfile = { + did: "did:key:web3studentlab-contributor", + walletAddress: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + githubHandle: "web3-student", + contributorTier: "newcomer", + skills: ["documentation", "testing", "code review"], +}; + +function toVerifiedIdentityMetadata( + attestation: IdentityVerificationAttestation, +): VerifiedIdentityMetadata { + return { + did: attestation.subject.did, + walletAddress: attestation.subject.walletAddress, + githubHandle: attestation.subject.githubHandle, + verifiedAt: attestation.issuedAt, + issuer: attestation.issuer, + method: attestation.method, + }; +} export default function VersionControlPage() { const [documents, setDocuments] = useState(() => - VersionControl.getAllDocuments() + VersionControl.getAllDocuments(), ); const [activeDocId, setActiveDocId] = useState(null); const [showCreate, setShowCreate] = useState(false); - const [newTitle, setNewTitle] = useState(''); - const [newContent, setNewContent] = useState(''); + const [newTitle, setNewTitle] = useState(""); + const [newContent, setNewContent] = useState(""); const [showHistory, setShowHistory] = useState(false); - const [editContent, setEditContent] = useState(''); - const [editTitle, setEditTitle] = useState(''); - const [commitMessage, setCommitMessage] = useState(''); - const [author, setAuthor] = useState('Current User'); + const [editContent, setEditContent] = useState(""); + const [editTitle, setEditTitle] = useState(""); + const [commitMessage, setCommitMessage] = useState(""); + const [author, setAuthor] = useState("Current User"); + const [identityProfile, setIdentityProfile] = + useState(DEFAULT_IDENTITY_PROFILE); + const [identityAttestation, setIdentityAttestation] = + useState(null); + const [identityResult, setIdentityResult] = + useState(null); + const [isVerifyingIdentity, setIsVerifyingIdentity] = useState(false); + const [identityError, setIdentityError] = useState(null); const activeDoc = useMemo( () => (activeDocId ? documents.find((d) => d.id === activeDocId) : null), - [activeDocId, documents] + [activeDocId, documents], + ); + + const latestVersion = useMemo( + () => (activeDoc ? VersionControl.getLatestVersion(activeDoc.id) : null), + [activeDoc], ); const refreshDocuments = useCallback(() => { setDocuments(VersionControl.getAllDocuments()); }, []); + useEffect(() => { + const storedAttestation = IdentityVerificationStore.load(); + if (!storedAttestation) return; + + setIdentityAttestation(storedAttestation); + setIdentityProfile(storedAttestation.subject); + + void verifyIdentityAttestation(storedAttestation).then((result) => { + setIdentityResult(result); + if (!result.isValid) { + IdentityVerificationStore.clear(); + setIdentityAttestation(null); + } + }); + }, []); + const handleCreate = useCallback(() => { if (!newTitle.trim() || !newContent.trim()) return; - VersionControl.createDocument(newTitle.trim(), newContent.trim(), author, 'Initial version'); - setNewTitle(''); - setNewContent(''); + VersionControl.createDocument( + newTitle.trim(), + newContent.trim(), + author, + "Initial version", + ); + setNewTitle(""); + setNewContent(""); setShowCreate(false); refreshDocuments(); }, [newTitle, newContent, author, refreshDocuments]); + const handleVerifyIdentity = useCallback(async () => { + setIsVerifyingIdentity(true); + setIdentityError(null); + + try { + const attestation = await createIdentityAttestation(identityProfile); + const result = await verifyIdentityAttestation(attestation); + + setIdentityAttestation(attestation); + setIdentityResult(result); + + if (!result.isValid) { + IdentityVerificationStore.clear(); + throw new Error(result.reason); + } + + IdentityVerificationStore.save(attestation); + } catch (error) { + setIdentityError( + error instanceof Error ? error.message : "Failed to verify identity.", + ); + } finally { + setIsVerifyingIdentity(false); + } + }, [identityProfile]); + + const handleClearIdentity = useCallback(() => { + IdentityVerificationStore.clear(); + setIdentityAttestation(null); + setIdentityResult(null); + setIdentityError(null); + }, []); + const handleSaveVersion = useCallback(() => { - if (!activeDocId || !editContent.trim()) return; + if ( + !activeDocId || + !editContent.trim() || + !identityAttestation || + !identityResult?.isValid + ) + return; + VersionControl.createVersion( activeDocId, - editTitle.trim() || activeDoc?.title || 'Untitled', + editTitle.trim() || activeDoc?.title || "Untitled", editContent, author, - commitMessage.trim() || 'Updated content' + commitMessage.trim() || "Updated content", + ["did-verified", identityProfile.contributorTier], + { + verifiedIdentity: toVerifiedIdentityMetadata(identityAttestation), + }, ); - setCommitMessage(''); + + setCommitMessage(""); refreshDocuments(); - }, [activeDocId, editContent, editTitle, activeDoc, author, commitMessage, refreshDocuments]); + }, [ + activeDocId, + editContent, + identityAttestation, + identityResult, + editTitle, + activeDoc, + author, + commitMessage, + identityProfile.contributorTier, + refreshDocuments, + ]); const handleRollback = useCallback( (version: Version) => { @@ -57,7 +191,7 @@ export default function VersionControlPage() { VersionControl.rollback(activeDocId, version.id, author); refreshDocuments(); }, - [activeDocId, author, refreshDocuments] + [activeDocId, author, refreshDocuments], ); const handleDelete = useCallback( @@ -69,39 +203,201 @@ export default function VersionControlPage() { } refreshDocuments(); }, - [activeDocId, refreshDocuments] + [activeDocId, refreshDocuments], ); - const openDocument = useCallback( - (doc: DocumentEntry) => { - const latest = VersionControl.getLatestVersion(doc.id); - if (latest) { - setEditTitle(latest.title); - setEditContent(latest.content); - } - setActiveDocId(doc.id); - setShowHistory(false); - }, - [] + const openDocument = useCallback((doc: DocumentEntry) => { + const latest = VersionControl.getLatestVersion(doc.id); + if (latest) { + setEditTitle(latest.title); + setEditContent(latest.content); + } + setActiveDocId(doc.id); + setShowHistory(false); + }, []); + + const isIdentityVerified = Boolean( + identityAttestation && identityResult?.isValid, ); + const latestVersionIdentity = latestVersion?.metadata.verifiedIdentity; return (
-
- Content management -

- Version Control -

-

- Track changes to learning materials with full version history, comparison, and rollback - capability. -

+
+
+ Open source contribution trainer +

+ Decentralized Identity Verification +

+

+ Verify contributor identity before submitting issue fixes, branch + updates, and pull-request training artifacts. Every saved version + carries a DID-backed verification record so the trainer can model + production-ready contribution workflows. +

+
+ +
+
+
+

+ Verification status +

+
+ {isIdentityVerified ? ( + <> + + Identity verified + + ) : ( + <> + + Verification required + + )} +
+

+ {identityResult?.reason || + "Link a contributor DID, Stellar wallet, and GitHub handle before saving a new trainer version."} +

+
+ {isIdentityVerified && ( + + MVP ready + + )} +
+ +
+ + + + +
+ +
+ {identityProfile.skills.map((skill) => ( + + {skill} + + ))} +
+ + {identityAttestation && ( +
+

+ + Issuer: + {" "} + {identityAttestation.issuer} +

+

+ + Challenge: + {" "} + {identityAttestation.challenge} +

+

+ + Expires: + {" "} + {new Date(identityAttestation.expiresAt).toLocaleString()} +

+
+ )} + + {identityError && ( +

{identityError}

+ )} + +
+ + +
+
-

Documents

+

+ Contribution drafts +