mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
migration to not require badges be linked to course, fixed in flow, testing out real plebdevs badges
This commit is contained in:
parent
85bce5544d
commit
5e579614d7
@ -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;
|
@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# 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"
|
provider = "postgresql"
|
@ -1,14 +1,14 @@
|
|||||||
// datasource db {
|
|
||||||
// provider = "postgresql"
|
|
||||||
// url = env("DATABASE_URL")
|
|
||||||
// }
|
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("POSTGRES_PRISMA_URL")
|
url = env("DATABASE_URL")
|
||||||
directUrl = env("POSTGRES_URL_NON_POOLING")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// datasource db {
|
||||||
|
// provider = "postgresql"
|
||||||
|
// url = env("POSTGRES_PRISMA_URL")
|
||||||
|
// directUrl = env("POSTGRES_URL_NON_POOLING")
|
||||||
|
// }
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
@ -254,8 +254,8 @@ model Badge {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
noteId String @unique
|
noteId String @unique
|
||||||
courseId String @unique // One badge per course
|
courseId String? @unique // Optional relation to course
|
||||||
course Course @relation(fields: [courseId], references: [id])
|
course Course? @relation(fields: [courseId], references: [id])
|
||||||
userBadges UserBadge[] // Many users can have this badge
|
userBadges UserBadge[] // Many users can have this badge
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
@ -133,12 +133,7 @@ const HeroBanner = () => {
|
|||||||
className="border-2"
|
className="border-2"
|
||||||
size={isMobile ? null : "large"}
|
size={isMobile ? null : "large"}
|
||||||
outlined
|
outlined
|
||||||
onClick={() => signIn('anonymous', {
|
onClick={() => router.push('/course/naddr1qvzqqqr4xspzpueu32tp0jc47uzlcuxdgcw06m40ytu7ynpna2adnqty3e0vda6pqy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uq3wamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmny9uq36amnwvaz7tmjv4kxz7fwd46hg6tw09mkzmrvv46zucm0d5hsz9mhwden5te0wfjkccte9ec8y6tdv9kzumn9wshszynhwden5te0dehhxarjxgcjucm0d5hszynhwden5te0dehhxarjw4jjucm0d5hsz9nhwden5te0wp6hyurvv4ex2mrp0yhxxmmd9uq3wamnwvaz7tmjv4kxz7fwv3jhvueww3hk7mrn9uqzge34xvuxvdtrx5knzcfhxgkngwpsxsknsetzxyknxe3sx43k2cfkxsurwdq68epwa?active=0')}
|
||||||
callbackUrl: '/profile',
|
|
||||||
redirect: true,
|
|
||||||
pubkey: null,
|
|
||||||
privkey: null
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<GenericButton
|
<GenericButton
|
||||||
label="Level Up"
|
label="Level Up"
|
||||||
|
@ -21,7 +21,7 @@ const UserBadges = ({ visible, onHide }) => {
|
|||||||
// Fetch badge definitions (kind 30009)
|
// Fetch badge definitions (kind 30009)
|
||||||
const badgeDefinitions = await ndk.fetchEvents({
|
const badgeDefinitions = await ndk.fetchEvents({
|
||||||
// todo: add the plebdevs hardcoded badge ids (probably in config?)
|
// todo: add the plebdevs hardcoded badge ids (probably in config?)
|
||||||
ids: ["97777aaccfb409ab973d30fc3a27de5ca64080c13a0bca6c2c261105ae545118"]
|
ids: ["4054a68f028edf38cd1d71cc4693d4ff5c9c54b0b44532361fe6abb29530cbf6", "5d38fea9a3c1fb4c55c9635c3132d34608c91de640f772438faa1942677087a8", "3ba20936d66523adb6d71793649bc77f3cea34f50c21ec7bb2c041f936022214", "41edee5af6d4e833d11f9411c2c27cc48c14d2a3c7966ae7648568e825eda1ed"]
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Badge Definitions: ", badgeDefinitions);
|
console.log("Badge Definitions: ", badgeDefinitions);
|
||||||
@ -30,7 +30,7 @@ const UserBadges = ({ visible, onHide }) => {
|
|||||||
const badgeAwards = await ndk.fetchEvents({
|
const badgeAwards = await ndk.fetchEvents({
|
||||||
kinds: [8],
|
kinds: [8],
|
||||||
// todo: add the plebdevs author pubkey
|
// todo: add the plebdevs author pubkey
|
||||||
authors: ["62bad2c804210b9ccd8b3d6b49da7333185bae17c12b6d7a8ed5865642e82b1e"],
|
authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"],
|
||||||
"#p": [session.user.pubkey]
|
"#p": [session.user.pubkey]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ const allTasks = [
|
|||||||
status: 'PlebDevs Starter',
|
status: 'PlebDevs Starter',
|
||||||
completed: false,
|
completed: false,
|
||||||
tier: 'Plebdev',
|
tier: 'Plebdev',
|
||||||
courseId: "f538f5c5-1a72-4804-8eb1-3f05cea64874",
|
// courseId: "f538f5c5-1a72-4804-8eb1-3f05cea64874",
|
||||||
// courseId: "5664e78f-c618-410d-a7cc-f3393b021fdf",
|
courseId: "5664e78f-c618-410d-a7cc-f3393b021fdf",
|
||||||
subTasks: [
|
subTasks: [
|
||||||
{ status: 'Complete the course', completed: false },
|
{ status: 'Complete the course', completed: false },
|
||||||
]
|
]
|
||||||
|
@ -12,7 +12,7 @@ export const useBadge = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.user || isProcessing || !completedCourses) return;
|
if (!session?.user || isProcessing) return;
|
||||||
|
|
||||||
const checkForBadgeEligibility = async () => {
|
const checkForBadgeEligibility = async () => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
@ -22,6 +22,29 @@ export const useBadge = () => {
|
|||||||
const { userBadges } = session.user;
|
const { userBadges } = session.user;
|
||||||
let badgesAwarded = false;
|
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 eligibleCourses = completedCourses?.filter(userCourse => {
|
||||||
const isCompleted = userCourse.completed;
|
const isCompleted = userCourse.completed;
|
||||||
const hasNoBadge = !userBadges?.some(
|
const hasNoBadge = !userBadges?.some(
|
||||||
@ -35,7 +58,7 @@ export const useBadge = () => {
|
|||||||
for (const course of eligibleCourses || []) {
|
for (const course of eligibleCourses || []) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/badges/issue', {
|
const response = await axios.post('/api/badges/issue', {
|
||||||
courseId: course.courseId,
|
courseId: course?.courseId,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,9 +26,11 @@ export default async function handler(req, res) {
|
|||||||
return res.status(401).json({ error: 'Unauthorized' });
|
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
|
let badge;
|
||||||
|
if (courseId && courseId !== null && courseId !== undefined) {
|
||||||
|
// Existing course badge logic
|
||||||
const userCourse = await prisma.userCourse.findFirst({
|
const userCourse = await prisma.userCourse.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
@ -41,7 +43,7 @@ export default async function handler(req, res) {
|
|||||||
badge: true,
|
badge: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: true, // Include user to get pubkey
|
user: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,22 +51,26 @@ export default async function handler(req, res) {
|
|||||||
return res.status(400).json({ error: 'Course not completed' });
|
return res.status(400).json({ error: 'Course not completed' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCourse.course.badge) {
|
badge = userCourse.course.badge;
|
||||||
return res.status(400).json({ error: 'No badge defined for this course' });
|
} else if (badgeId) {
|
||||||
|
// Direct badge lookup for non-course badges
|
||||||
|
badge = await prisma.badge.findUnique({
|
||||||
|
where: { id: badgeId },
|
||||||
|
include: { userBadges: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!badge) {
|
||||||
|
return res.status(400).json({ error: 'Badge not found' });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
let noteId = userCourse.course.badge.noteId;
|
return res.status(400).json({ error: 'Either courseId or badgeId is required' });
|
||||||
|
|
||||||
if (noteId && noteId.startsWith("naddr")) {
|
|
||||||
const naddr = nip19.decode(noteId);
|
|
||||||
noteId = `${naddr.data.kind}:${naddr.data.pubkey}:${naddr.data.identifier}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if badge already exists
|
// Check if badge already exists
|
||||||
const existingBadge = await prisma.userBadge.findFirst({
|
const existingBadge = await prisma.userBadge.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
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' });
|
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
|
// Get the signing key from environment and convert to bytes
|
||||||
const signingKey = process.env.BADGE_SIGNING_KEY;
|
const signingKey = process.env.BADGE_SIGNING_KEY;
|
||||||
if (!signingKey) {
|
if (!signingKey) {
|
||||||
@ -84,21 +102,15 @@ export default async function handler(req, res) {
|
|||||||
kind: BADGE_AWARD_KIND,
|
kind: BADGE_AWARD_KIND,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
tags: [
|
tags: [
|
||||||
['p', userCourse.user.pubkey],
|
['p', user.pubkey],
|
||||||
['a', noteId],
|
['a', noteId],
|
||||||
['d', `course-completion-${userCourse.course.id}`],
|
['d', `plebdevs-badge-award-${session.user.id}`],
|
||||||
],
|
],
|
||||||
content: JSON.stringify({
|
content: ""
|
||||||
name: userCourse.course.badge.name,
|
|
||||||
description: `Completed ${userCourse.course.id}`,
|
|
||||||
image: userCourse.course.badge.noteId,
|
|
||||||
course: courseId,
|
|
||||||
awardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add validation for required fields
|
// Add validation for required fields
|
||||||
if (!userCourse.user.pubkey || !noteId) {
|
if (!user.pubkey || !noteId) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Missing required fields',
|
error: 'Missing required fields',
|
||||||
message: 'Pubkey and noteId are required'
|
message: 'Pubkey and noteId are required'
|
||||||
@ -108,6 +120,8 @@ export default async function handler(req, res) {
|
|||||||
// Finalize (sign) the event
|
// Finalize (sign) the event
|
||||||
const signedEvent = finalizeEvent(eventTemplate, signingKeyBytes);
|
const signedEvent = finalizeEvent(eventTemplate, signingKeyBytes);
|
||||||
|
|
||||||
|
console.log("Signed Event: ", signedEvent);
|
||||||
|
|
||||||
// Verify the event
|
// Verify the event
|
||||||
const isValid = verifyEvent(signedEvent);
|
const isValid = verifyEvent(signedEvent);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@ -136,7 +150,7 @@ export default async function handler(req, res) {
|
|||||||
const userBadge = await prisma.userBadge.create({
|
const userBadge = await prisma.userBadge.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
badgeId: userCourse.course.badge.id,
|
badgeId: badge.id,
|
||||||
awardedAt: new Date(),
|
awardedAt: new Date(),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user