diff --git a/prisma/migrations/20241230221315_badges_optional_course_relation/migration.sql b/prisma/migrations/20241230221315_badges_optional_course_relation/migration.sql new file mode 100644 index 0000000..bb12afb --- /dev/null +++ b/prisma/migrations/20241230221315_badges_optional_course_relation/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Badge" DROP CONSTRAINT "Badge_courseId_fkey"; + +-- AlterTable +ALTER TABLE "Badge" ALTER COLUMN "courseId" DROP NOT NULL; + +-- AddForeignKey +ALTER TABLE "Badge" ADD CONSTRAINT "Badge_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index fbffa92..648c57f 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index aa8be5c..ec11f45 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,14 +1,14 @@ -// datasource db { -// provider = "postgresql" -// url = env("DATABASE_URL") -// } - datasource db { - provider = "postgresql" - url = env("POSTGRES_PRISMA_URL") - directUrl = env("POSTGRES_URL_NON_POOLING") + provider = "postgresql" + url = env("DATABASE_URL") } +// datasource db { +// provider = "postgresql" +// url = env("POSTGRES_PRISMA_URL") +// directUrl = env("POSTGRES_URL_NON_POOLING") +// } + generator client { provider = "prisma-client-js" } @@ -254,8 +254,8 @@ model Badge { id String @id @default(uuid()) name String noteId String @unique - courseId String @unique // One badge per course - course Course @relation(fields: [courseId], references: [id]) + courseId String? @unique // Optional relation to course + course Course? @relation(fields: [courseId], references: [id]) userBadges UserBadge[] // Many users can have this badge createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/components/banner/HeroBanner.js b/src/components/banner/HeroBanner.js index 426e39c..a8f3b1c 100644 --- a/src/components/banner/HeroBanner.js +++ b/src/components/banner/HeroBanner.js @@ -133,12 +133,7 @@ const HeroBanner = () => { className="border-2" size={isMobile ? null : "large"} outlined - onClick={() => signIn('anonymous', { - callbackUrl: '/profile', - redirect: true, - pubkey: null, - privkey: null - })} + onClick={() => router.push('/course/naddr1qvzqqqr4xspzpueu32tp0jc47uzlcuxdgcw06m40ytu7ynpna2adnqty3e0vda6pqy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uq3wamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmny9uq36amnwvaz7tmjv4kxz7fwd46hg6tw09mkzmrvv46zucm0d5hsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshszynhwden5te0dehhxarjxgcjucm0d5hszynhwden5te0dehhxarjw4jjucm0d5hsz9nhwden5te0wp6hyurvv4ex2mrp0yhxxmmd9uq3wamnwvaz7tmjv4kxz7fwv3jhvueww3hk7mrn9uqzge34xvuxvdtrx5knzcfhxgkngwpsxsknsetzxyknxe3sx43k2cfkxsurwdq68epwa?active=0')} /> { // Fetch badge definitions (kind 30009) const badgeDefinitions = await ndk.fetchEvents({ // todo: add the plebdevs hardcoded badge ids (probably in config?) - ids: ["97777aaccfb409ab973d30fc3a27de5ca64080c13a0bca6c2c261105ae545118"] + ids: ["4054a68f028edf38cd1d71cc4693d4ff5c9c54b0b44532361fe6abb29530cbf6", "5d38fea9a3c1fb4c55c9635c3132d34608c91de640f772438faa1942677087a8", "3ba20936d66523adb6d71793649bc77f3cea34f50c21ec7bb2c041f936022214", "41edee5af6d4e833d11f9411c2c27cc48c14d2a3c7966ae7648568e825eda1ed"] }); console.log("Badge Definitions: ", badgeDefinitions); @@ -30,7 +30,7 @@ const UserBadges = ({ visible, onHide }) => { const badgeAwards = await ndk.fetchEvents({ kinds: [8], // todo: add the plebdevs author pubkey - authors: ["62bad2c804210b9ccd8b3d6b49da7333185bae17c12b6d7a8ed5865642e82b1e"], + authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"], "#p": [session.user.pubkey] }); diff --git a/src/components/profile/progress/UserProgress.js b/src/components/profile/progress/UserProgress.js index ff1442d..58c42bd 100644 --- a/src/components/profile/progress/UserProgress.js +++ b/src/components/profile/progress/UserProgress.js @@ -22,8 +22,8 @@ const allTasks = [ status: 'PlebDevs Starter', completed: false, tier: 'Plebdev', - courseId: "f538f5c5-1a72-4804-8eb1-3f05cea64874", - // courseId: "5664e78f-c618-410d-a7cc-f3393b021fdf", + // courseId: "f538f5c5-1a72-4804-8eb1-3f05cea64874", + courseId: "5664e78f-c618-410d-a7cc-f3393b021fdf", subTasks: [ { status: 'Complete the course', completed: false }, ] diff --git a/src/hooks/badges/useBadge.js b/src/hooks/badges/useBadge.js index 61ad1d7..13906a1 100644 --- a/src/hooks/badges/useBadge.js +++ b/src/hooks/badges/useBadge.js @@ -12,7 +12,7 @@ export const useBadge = () => { const queryClient = useQueryClient(); useEffect(() => { - if (!session?.user || isProcessing || !completedCourses) return; + if (!session?.user || isProcessing) return; const checkForBadgeEligibility = async () => { setIsProcessing(true); @@ -22,6 +22,29 @@ export const useBadge = () => { const { userBadges } = session.user; let badgesAwarded = false; + // Check for GitHub connection badge + if (session?.account?.provider === 'github') { + const hasPlebBadge = userBadges?.some( + userBadge => userBadge.badge?.id === '3664e78f-b618-420d-a7cc-f3393b0211df' + ); + + if (!hasPlebBadge) { + try { + const response = await axios.post('/api/badges/issue', { + badgeId: '3664e78f-b618-420d-a7cc-f3393b0211df', + userId: session.user.id, + }); + + if (response.data.success) { + badgesAwarded = true; + } + } catch (error) { + console.error('Error issuing Pleb badge:', error); + } + } + } + + // Check for course-related badges const eligibleCourses = completedCourses?.filter(userCourse => { const isCompleted = userCourse.completed; const hasNoBadge = !userBadges?.some( @@ -35,7 +58,7 @@ export const useBadge = () => { for (const course of eligibleCourses || []) { try { const response = await axios.post('/api/badges/issue', { - courseId: course.courseId, + courseId: course?.courseId, userId: session.user.id, }); diff --git a/src/pages/api/badges/issue.js b/src/pages/api/badges/issue.js index e3e3bd5..69088b7 100644 --- a/src/pages/api/badges/issue.js +++ b/src/pages/api/badges/issue.js @@ -26,45 +26,51 @@ export default async function handler(req, res) { return res.status(401).json({ error: 'Unauthorized' }); } - const { courseId, userId } = req.body; + const { courseId, badgeId, userId } = req.body; - // Verify course completion and get badge details - const userCourse = await prisma.userCourse.findFirst({ - where: { - userId, - courseId, - completed: true, - }, - include: { - course: { - include: { - badge: true, - }, + let badge; + if (courseId && courseId !== null && courseId !== undefined) { + // Existing course badge logic + const userCourse = await prisma.userCourse.findFirst({ + where: { + userId, + courseId, + completed: true, }, - user: true, // Include user to get pubkey - }, - }); + include: { + course: { + include: { + badge: true, + }, + }, + user: true, + }, + }); - if (!userCourse) { - return res.status(400).json({ error: 'Course not completed' }); - } + if (!userCourse) { + return res.status(400).json({ error: 'Course not completed' }); + } - if (!userCourse.course.badge) { - return res.status(400).json({ error: 'No badge defined for this course' }); - } + badge = userCourse.course.badge; + } else if (badgeId) { + // Direct badge lookup for non-course badges + badge = await prisma.badge.findUnique({ + where: { id: badgeId }, + include: { userBadges: true }, + }); - let noteId = userCourse.course.badge.noteId; - - if (noteId && noteId.startsWith("naddr")) { - const naddr = nip19.decode(noteId); - noteId = `${naddr.data.kind}:${naddr.data.pubkey}:${naddr.data.identifier}`; + if (!badge) { + return res.status(400).json({ error: 'Badge not found' }); + } + } else { + return res.status(400).json({ error: 'Either courseId or badgeId is required' }); } // Check if badge already exists const existingBadge = await prisma.userBadge.findFirst({ where: { userId, - badgeId: userCourse.course.badge.id, + badgeId: badge.id, }, }); @@ -72,6 +78,18 @@ export default async function handler(req, res) { return res.status(400).json({ error: 'Badge already awarded' }); } + // Get user for pubkey + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + let noteId = badge.noteId; + + if (noteId && noteId.startsWith("naddr")) { + const naddr = nip19.decode(noteId); + noteId = `${naddr.data.kind}:${naddr.data.pubkey}:${naddr.data.identifier}`; + } + // Get the signing key from environment and convert to bytes const signingKey = process.env.BADGE_SIGNING_KEY; if (!signingKey) { @@ -84,21 +102,15 @@ export default async function handler(req, res) { kind: BADGE_AWARD_KIND, created_at: Math.floor(Date.now() / 1000), tags: [ - ['p', userCourse.user.pubkey], + ['p', user.pubkey], ['a', noteId], - ['d', `course-completion-${userCourse.course.id}`], + ['d', `plebdevs-badge-award-${session.user.id}`], ], - content: JSON.stringify({ - name: userCourse.course.badge.name, - description: `Completed ${userCourse.course.id}`, - image: userCourse.course.badge.noteId, - course: courseId, - awardedAt: new Date().toISOString(), - }) + content: "" }; // Add validation for required fields - if (!userCourse.user.pubkey || !noteId) { + if (!user.pubkey || !noteId) { return res.status(400).json({ error: 'Missing required fields', message: 'Pubkey and noteId are required' @@ -108,6 +120,8 @@ export default async function handler(req, res) { // Finalize (sign) the event const signedEvent = finalizeEvent(eventTemplate, signingKeyBytes); + console.log("Signed Event: ", signedEvent); + // Verify the event const isValid = verifyEvent(signedEvent); if (!isValid) { @@ -136,7 +150,7 @@ export default async function handler(req, res) { const userBadge = await prisma.userBadge.create({ data: { userId, - badgeId: userCourse.course.badge.id, + badgeId: badge.id, awardedAt: new Date(), }, include: {