migration to not require badges be linked to course, fixed in flow, testing out real plebdevs badges

This commit is contained in:
austinkelsay 2024-12-30 16:14:14 -06:00
parent 85bce5544d
commit 5e579614d7
No known key found for this signature in database
GPG Key ID: 44CB4EC6D9F2FA02
8 changed files with 102 additions and 62 deletions

View File

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

View File

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

View File

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

View File

@ -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')}
/>
<GenericButton
label="Level Up"

View File

@ -21,7 +21,7 @@ const UserBadges = ({ visible, onHide }) => {
// 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]
});

View File

@ -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 },
]

View File

@ -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,
});

View File

@ -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: {