mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Merge pull request #5 from AustinKelsay/feature/account-linking-component
Feature- account linking component
This commit is contained in:
commit
8250c34e04
BIN
public/images/nostr-icon-white.png
Normal file
BIN
public/images/nostr-icon-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
19
src/components/profile/BitcoinLightningCard.js
Normal file
19
src/components/profile/BitcoinLightningCard.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import BitcoinConnectButton from '@/components/bitcoinConnect/BitcoinConnect';
|
||||
|
||||
const BitcoinLightningCard = () => {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-4 my-2 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<i className="pi pi-bolt text-yellow-500 text-2xl"></i>
|
||||
<h3 className="text-xl font-semibold">Lightning Wallet Connection</h3>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-4">
|
||||
Connect your Lightning wallet for easier payments across the platform
|
||||
</p>
|
||||
<BitcoinConnectButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BitcoinLightningCard;
|
201
src/components/profile/UserAccountLinking.js
Normal file
201
src/components/profile/UserAccountLinking.js
Normal file
@ -0,0 +1,201 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Menu } from 'primereact/menu';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
import useWindowWidth from '@/hooks/useWindowWidth';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import MoreInfo from '@/components/MoreInfo';
|
||||
|
||||
const LinkAccountsCard = ({ session }) => {
|
||||
const isNostrLinked = session?.user?.pubkey && !session?.user?.privkey;
|
||||
const isGithubLinked = session?.account?.provider === 'github';
|
||||
const isEmailLinked = Boolean(session?.user?.email);
|
||||
const windowWidth = useWindowWidth();
|
||||
const { ndk, addSigner } = useNDKContext();
|
||||
const { showToast } = useToast();
|
||||
const router = useRouter();
|
||||
const { update } = useSession();
|
||||
|
||||
// Check for email verification success
|
||||
useEffect(() => {
|
||||
const checkEmailVerification = async () => {
|
||||
if (router.query.emailVerified === 'true') {
|
||||
await update(); // Update the session
|
||||
showToast('success', 'Success', 'Email verified successfully');
|
||||
// Remove the query parameter
|
||||
router.replace('/profile', undefined, { shallow: true });
|
||||
} else if (router.query.error === 'VerificationFailed') {
|
||||
showToast('error', 'Error', 'Email verification failed');
|
||||
router.replace('/profile', undefined, { shallow: true });
|
||||
}
|
||||
};
|
||||
|
||||
checkEmailVerification();
|
||||
}, [router.query, update, showToast, router]);
|
||||
|
||||
const handleGithubLink = async () => {
|
||||
if (!isGithubLinked) {
|
||||
try {
|
||||
await signIn("github", {
|
||||
redirect: false,
|
||||
// Pass existing user data for linking
|
||||
userId: session?.user?.id,
|
||||
pubkey: session?.user?.pubkey,
|
||||
privkey: session?.user?.privkey || null
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error linking GitHub:", error);
|
||||
showToast('error', 'Error', 'Failed to link GitHub account');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNostrLink = async () => {
|
||||
if (!isNostrLinked) {
|
||||
try {
|
||||
if (!ndk.signer) {
|
||||
await addSigner();
|
||||
}
|
||||
const user = await ndk.signer.user();
|
||||
const pubkey = user?._pubkey;
|
||||
|
||||
if (pubkey) {
|
||||
const response = await fetch('/api/user/link-nostr', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nostrPubkey: pubkey,
|
||||
userId: session?.user?.id
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('success', 'Success', 'Nostr account linked successfully');
|
||||
// Refresh the session to get updated user data
|
||||
await update();
|
||||
} else {
|
||||
throw new Error('Failed to link Nostr account');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error linking Nostr:", error);
|
||||
showToast('error', 'Error', 'Failed to link Nostr account');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailLink = async () => {
|
||||
if (!isEmailLinked) {
|
||||
try {
|
||||
const email = prompt("Please enter your email address:");
|
||||
if (email) {
|
||||
const response = await fetch('/api/user/link-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
userId: session?.user?.id
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('success', 'Success', 'Verification email sent');
|
||||
// The user will need to verify their email through the link sent
|
||||
} else {
|
||||
throw new Error('Failed to initiate email linking');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error linking email:", error);
|
||||
showToast('error', 'Error', 'Failed to link email');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MobileCard = () => (
|
||||
<div className="bg-gray-800 rounded-xl p-6 flex flex-col items-start w-full border border-gray-700 my-2">
|
||||
<div className="flex flex-row justify-between items-center w-full mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Link Accounts</h2>
|
||||
<MoreInfo title="Link Accounts" description="Link your accounts to your profile. You can link your Github, Nostr, and Email accounts to your profile to ensure you can access your account from any device." />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<GenericButton
|
||||
label={isNostrLinked ? "Nostr Linked" : "Link Nostr"}
|
||||
icon={<Image src="/images/nostr-icon-white.png" width={20} height={20} alt="Nostr" className="mr-2" />}
|
||||
onClick={handleNostrLink}
|
||||
disabled={isNostrLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto flex items-center justify-center`}
|
||||
rounded
|
||||
/>
|
||||
|
||||
<GenericButton
|
||||
label={isGithubLinked ? "Github Linked" : "Link Github"}
|
||||
icon="pi pi-github"
|
||||
onClick={handleGithubLink}
|
||||
disabled={isGithubLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto`}
|
||||
rounded
|
||||
/>
|
||||
|
||||
<GenericButton
|
||||
label={isEmailLinked ? "Email Linked" : "Link Email"}
|
||||
icon="pi pi-envelope"
|
||||
onClick={handleEmailLink}
|
||||
disabled={isEmailLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto`}
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DesktopCard = () => (
|
||||
<div className="bg-gray-800 rounded-xl p-6 flex flex-col items-start w-full max-w-[400px] mt-2 border border-gray-700">
|
||||
<div className="flex flex-row justify-between items-center w-full mb-6">
|
||||
<h2 className="text-2xl font-bold text-white">Link Accounts</h2>
|
||||
<MoreInfo title="Link Accounts" description="Link your accounts to your profile. You can link your Github, Nostr, and Email accounts to your profile to ensure you can access your account from any device." />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<GenericButton
|
||||
label={isGithubLinked ? "Github Linked" : "Link Github"}
|
||||
icon="pi pi-github"
|
||||
onClick={handleGithubLink}
|
||||
disabled={isGithubLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto`}
|
||||
rounded
|
||||
/>
|
||||
|
||||
<GenericButton
|
||||
label={isNostrLinked ? "Nostr Linked" : "Link Nostr"}
|
||||
icon={<Image src="/images/nostr-icon-white.png" width={20} height={20} alt="Nostr" className="mr-2" />}
|
||||
onClick={handleNostrLink}
|
||||
disabled={isNostrLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto flex items-center justify-center`}
|
||||
rounded
|
||||
/>
|
||||
|
||||
<GenericButton
|
||||
label={isEmailLinked ? "Email Linked" : "Link Email"}
|
||||
icon="pi pi-envelope"
|
||||
onClick={handleEmailLink}
|
||||
disabled={isEmailLinked}
|
||||
className={`text-[#f8f8ff] w-[250px] mx-auto`}
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return windowWidth <= 1440 ? <MobileCard /> : <DesktopCard />;
|
||||
};
|
||||
|
||||
export default LinkAccountsCard;
|
@ -9,6 +9,7 @@ import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import UserProgress from "@/components/profile/progress/UserProgress";
|
||||
import UserProgressTable from '@/components/profile/DataTables/UserProgressTable';
|
||||
import UserPurchaseTable from '@/components/profile/DataTables/UserPurchaseTable';
|
||||
import BitcoinLightningCard from '@/components/profile/BitcoinLightningCard';
|
||||
|
||||
const UserProfile = () => {
|
||||
const windowWidth = useWindowWidth();
|
||||
@ -40,6 +41,7 @@ const UserProfile = () => {
|
||||
<div className="w-full flex flex-row max-lap:flex-col">
|
||||
<div className="w-[22%] h-full max-lap:w-full">
|
||||
{user && <UserProfileCard user={user} />}
|
||||
<BitcoinLightningCard />
|
||||
</div>
|
||||
|
||||
<div className="w-[78%] flex flex-col justify-center mx-auto max-lap:w-full">
|
||||
|
@ -156,7 +156,7 @@ const UserProfileCard = ({ user }) => {
|
||||
className="rounded-full my-4"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 pt-4 w-fit relative">
|
||||
<div className="absolute top-[-1px] right-[-24px]">
|
||||
<div className="absolute top-[-1px] right-[-18px]">
|
||||
<i
|
||||
className="pi pi-ellipsis-h text-2xl cursor-pointer"
|
||||
onClick={(e) => menu.current.toggle(e)}
|
||||
|
@ -1,18 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { DataTable } from "primereact/datatable";
|
||||
import { Column } from "primereact/column";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import UserProfileCard from "@/components/profile/UserProfileCard";
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
|
||||
import { InputText } from "primereact/inputtext";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import SubscribeModal from "@/components/profile/subscription/SubscribeModal";
|
||||
import appConfig from "@/config/appConfig";
|
||||
import UserRelaysTable from "@/components/profile/DataTables/UserRelaysTable";
|
||||
|
||||
import UserAccountLinking from "@/components/profile/UserAccountLinking";
|
||||
const UserSettings = () => {
|
||||
const [user, setUser] = useState(null);
|
||||
const { ndk, userRelays, setUserRelays, reInitializeNDK } = useNDKContext();
|
||||
@ -35,20 +27,7 @@ const UserSettings = () => {
|
||||
<div className="w-[22%] h-full max-lap:w-full">
|
||||
<UserProfileCard user={user} />
|
||||
|
||||
{/* Lightning Info Card */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 my-4 border border-gray-700">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<i className="pi pi-bolt text-yellow-500 text-2xl"></i>
|
||||
<h3 className="text-xl font-semibold">Lightning Wallet Connection</h3>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-4">
|
||||
Connect your Lightning wallet for easier payments across the platform
|
||||
</p>
|
||||
<BitcoinConnectButton />
|
||||
</div>
|
||||
|
||||
{/* Subscription Modal */}
|
||||
{user && <SubscribeModal user={user} />}
|
||||
{user && <UserAccountLinking session={session} />}
|
||||
</div>
|
||||
|
||||
<div className="w-[78%] flex flex-col justify-center mx-auto max-lap:w-full ml-2 max-lap:ml-0">
|
||||
|
@ -148,7 +148,7 @@ const SubscribeModal = ({ user }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card title={subscriptionCardTitle} className="w-full m-4 mx-auto border border-gray-700">
|
||||
<Card title={subscriptionCardTitle} className="w-full m-4 mt-2 mx-auto border border-gray-700">
|
||||
{subscribed && !user?.role?.nwc && (
|
||||
<div className="flex flex-col">
|
||||
<Message className="w-fit" severity="success" text="Subscribed!" />
|
||||
@ -219,10 +219,6 @@ const SubscribeModal = ({ user }) => {
|
||||
<span>Claim your own personal plebdevs.com Nostr NIP-05 identity</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center mb-4 flex flex-row justify-center">
|
||||
<Badge value="BONUS" severity="success" className="mr-2 text-[#f8f8ff] font-bold"></Badge>
|
||||
<span className="text-center font-bold">I WILL MAKE SURE YOU WIN HARD AND LEVEL UP AS A DEV!</span>
|
||||
</div>
|
||||
<SubscriptionPaymentButtons
|
||||
onSuccess={handleSubscriptionSuccess}
|
||||
onRecurringSubscriptionSuccess={handleRecurringSubscriptionSuccess}
|
||||
|
@ -5,9 +5,10 @@ import { useToast } from '@/hooks/useToast';
|
||||
import axios from 'axios';
|
||||
import { Card } from 'primereact/card';
|
||||
import useWindowWidth from '@/hooks/useWindowWidth';
|
||||
import { Message } from "primereact/message";
|
||||
import SubscribeModal from '@/components/profile/subscription/SubscribeModal';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import SubscriptionPaymentButtons from '@/components/bitcoinConnect/SubscriptionPaymentButton';
|
||||
import UserProfileCard from '@/components/profile/UserProfileCard';
|
||||
import Image from 'next/image';
|
||||
import NostrIcon from '../../../../public/images/nostr.png';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
@ -28,6 +29,7 @@ const UserSubscription = () => {
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
const [subscribedUntil, setSubscribedUntil] = useState(null);
|
||||
const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null);
|
||||
const [subscribeModalVisible, setSubscribeModalVisible] = useState(false);
|
||||
const [calendlyVisible, setCalendlyVisible] = useState(false);
|
||||
const [lightningAddressVisible, setLightningAddressVisible] = useState(false);
|
||||
const [nip05Visible, setNip05Visible] = useState(false);
|
||||
@ -101,39 +103,28 @@ const UserSubscription = () => {
|
||||
<div className="w-full flex flex-row max-lap:flex-col">
|
||||
{/* Left Column - 22% */}
|
||||
<div className="w-[21%] h-full max-lap:w-full">
|
||||
<div className="p-4 bg-gray-800 rounded-lg max-lap:mb-4">
|
||||
{/* Subscription Status Messages */}
|
||||
{subscribed && !user?.role?.nwc && (
|
||||
<div className="flex flex-col">
|
||||
<Message className="w-fit" severity="success" text="Subscribed!" />
|
||||
<p className="mt-4">Thank you for your support 🎉</p>
|
||||
<p className="text-sm text-gray-400">Pay-as-you-go subscription requires manual renewal on {subscribedUntil.toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
{subscribed && user?.role?.nwc && (
|
||||
<div className="flex flex-col">
|
||||
<Message className="w-fit" severity="success" text="Subscribed!" />
|
||||
<p className="mt-4">Thank you for your support 🎉</p>
|
||||
<p className="text-sm text-gray-400">Recurring subscription will AUTO renew on {subscribedUntil.toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
{(!subscribed && !subscriptionExpiredAt) && (
|
||||
<div className="flex flex-col">
|
||||
<Message className="w-fit" severity="info" text="You currently have no active subscription" />
|
||||
</div>
|
||||
)}
|
||||
{subscriptionExpiredAt && (
|
||||
<div className="flex flex-col">
|
||||
<Message className="w-fit" severity="warn" text={`Your subscription expired on ${subscriptionExpiredAt.toLocaleDateString()}`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{user && (
|
||||
<>
|
||||
<UserProfileCard user={user} />
|
||||
<SubscribeModal
|
||||
visible={subscribeModalVisible}
|
||||
onHide={() => setSubscribeModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - 78% */}
|
||||
<div className="w-[78%] flex flex-col justify-center mx-auto max-lap:w-full">
|
||||
{!subscribed && (
|
||||
<Card title="Subscribe to PlebDevs" className="mb-4">
|
||||
<Card
|
||||
title="Subscribe to PlebDevs"
|
||||
className="mb-2 h-[330px] max-lap:h-auto"
|
||||
pt={{
|
||||
body: { className: 'py-2' },
|
||||
content: { className: 'pt-0' }
|
||||
}}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
|
||||
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
|
||||
@ -141,11 +132,10 @@ const UserSubscription = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-bold text-primary">Unlock Premium Benefits</h2>
|
||||
<div className="mb-2">
|
||||
<p className="text-gray-400">Subscribe now and elevate your development journey!</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 mb-4">
|
||||
<div className="flex flex-col gap-4 mb-2">
|
||||
<div className="flex items-center">
|
||||
<i className="pi pi-book text-2xl text-primary mr-2 text-blue-400"></i>
|
||||
<span>Access ALL current and future PlebDevs content</span>
|
||||
@ -162,10 +152,6 @@ const UserSubscription = () => {
|
||||
<Image src={NostrIcon} alt="Nostr" width={25} height={25} className='mr-2' />
|
||||
<span>Claim your own personal plebdevs.com Nostr NIP-05 identity</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<i className="pi pi-star text-2xl text-primary mr-2 text-yellow-500"></i>
|
||||
<span>I WILL MAKE SURE YOU WIN HARD AND LEVEL UP AS A DEV!</span>
|
||||
</div>
|
||||
</div>
|
||||
<SubscriptionPaymentButtons
|
||||
onSuccess={handleSubscriptionSuccess}
|
||||
|
@ -274,6 +274,7 @@ export const authOptions = {
|
||||
username: fullUser.username,
|
||||
avatar: fullUser.avatar,
|
||||
name: fullUser.name,
|
||||
email: fullUser.email,
|
||||
userCourses: fullUser.userCourses,
|
||||
userLessons: fullUser.userLessons,
|
||||
purchased: fullUser.purchased,
|
||||
|
84
src/pages/api/user/link-email.js
Normal file
84
src/pages/api/user/link-email.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import prisma from "@/db/prisma";
|
||||
import { createTransport } from "nodemailer";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { email, userId } = req.body;
|
||||
|
||||
// Check if email is already in use
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Email already in use' });
|
||||
}
|
||||
|
||||
// Create verification token
|
||||
const token = await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier: email,
|
||||
token: `${Math.random().toString(36).substring(2, 15)}`,
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
||||
}
|
||||
});
|
||||
|
||||
// Create email transport
|
||||
const transport = createTransport({
|
||||
host: process.env.EMAIL_SERVER_HOST,
|
||||
port: process.env.EMAIL_SERVER_PORT,
|
||||
auth: {
|
||||
user: process.env.EMAIL_SERVER_USER,
|
||||
pass: process.env.EMAIL_SERVER_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
// Generate verification URL
|
||||
const baseUrl = process.env.BACKEND_URL;
|
||||
const verificationUrl = `${baseUrl}/api/user/verify-email?token=${token.token}&email=${email}&userId=${userId}`;
|
||||
|
||||
// Send verification email
|
||||
await transport.sendMail({
|
||||
to: email,
|
||||
from: process.env.EMAIL_FROM,
|
||||
subject: `Verify your email for PlebDevs`,
|
||||
text: `Click this link to verify your email: ${verificationUrl}`,
|
||||
html: `
|
||||
<body>
|
||||
<div style="background: #f9f9f9; padding: 20px;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: #fff; padding: 20px; border-radius: 10px;">
|
||||
<h2>Verify your email for PlebDevs</h2>
|
||||
<p>Click the button below to verify your email address:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${verificationUrl}"
|
||||
style="background: #4A5568; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
||||
Verify Email
|
||||
</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
If you didn't request this email, you can safely ignore it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`
|
||||
});
|
||||
|
||||
// Don't update the user yet - wait for verification
|
||||
res.status(200).json({ message: 'Verification email sent' });
|
||||
} catch (error) {
|
||||
console.error('Error linking email:', error);
|
||||
res.status(500).json({ error: 'Failed to link email' });
|
||||
}
|
||||
}
|
32
src/pages/api/user/link-nostr.js
Normal file
32
src/pages/api/user/link-nostr.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../auth/[...nextauth]";
|
||||
import prisma from "@/db/prisma";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { nostrPubkey, userId } = req.body;
|
||||
|
||||
// Update user with new Nostr pubkey
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
pubkey: nostrPubkey,
|
||||
privkey: null // Remove privkey when linking to external Nostr account
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('Error linking Nostr:', error);
|
||||
res.status(500).json({ error: 'Failed to link Nostr account' });
|
||||
}
|
||||
}
|
46
src/pages/api/user/verify-email.js
Normal file
46
src/pages/api/user/verify-email.js
Normal file
@ -0,0 +1,46 @@
|
||||
import prisma from "@/db/prisma";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { token, email, userId } = req.query;
|
||||
|
||||
// Verify token
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
token,
|
||||
identifier: email,
|
||||
expires: { gt: new Date() }
|
||||
}
|
||||
});
|
||||
|
||||
if (!verificationToken) {
|
||||
return res.status(400).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
// Update user with verified email
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
email,
|
||||
emailVerified: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Delete the used token using the token value
|
||||
await prisma.verificationToken.delete({
|
||||
where: {
|
||||
token: token // Changed from id to token
|
||||
}
|
||||
});
|
||||
|
||||
// Redirect to success page
|
||||
res.redirect('/profile?emailVerified=true');
|
||||
} catch (error) {
|
||||
console.error('Error verifying email:', error);
|
||||
res.redirect('/profile?error=VerificationFailed');
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react"
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import Image from "next/image";
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
|
||||
export default function SignIn() {
|
||||
@ -101,7 +102,7 @@ export default function SignIn() {
|
||||
<h1 className="text-center mb-8">Sign In</h1>
|
||||
<GenericButton
|
||||
label={"login with nostr"}
|
||||
icon="pi pi-user"
|
||||
icon={<Image src="/images/nostr-icon-white.png" width={20} height={20} alt="Nostr" className="mr-2" />}
|
||||
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
|
||||
rounded
|
||||
onClick={handleNostrSignIn}
|
||||
|
@ -225,10 +225,6 @@ const Subscribe = () => {
|
||||
<Image src={NostrIcon} alt="Nostr" width={25} height={25} className='mr-2' />
|
||||
<span>Claim your own personal plebdevs.com Nostr NIP-05 identity</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<i className="pi pi-star text-2xl text-primary mr-2 text-yellow-500"></i>
|
||||
<span>I WILL MAKE SURE YOU WIN HARD AND LEVEL UP AS A DEV!</span>
|
||||
</div>
|
||||
</div>
|
||||
<SubscriptionPaymentButtons
|
||||
onSuccess={handleSubscriptionSuccess}
|
||||
|
Loading…
x
Reference in New Issue
Block a user