Consolidate user fields, differentiate platform lightning address and nip05, fix millisats on lightning address, small user management and signup ux fixes

This commit is contained in:
austinkelsay 2025-02-17 12:50:32 -06:00
parent 609459588d
commit bde5e117cf
27 changed files with 298 additions and 149 deletions

View File

@ -0,0 +1,63 @@
/*
Warnings:
- You are about to drop the `LightningAddress` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Nip05` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "LightningAddress" DROP CONSTRAINT "LightningAddress_userId_fkey";
-- DropForeignKey
ALTER TABLE "Nip05" DROP CONSTRAINT "Nip05_userId_fkey";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "lud16" TEXT,
ADD COLUMN "nip05" TEXT;
-- DropTable
DROP TABLE "LightningAddress";
-- DropTable
DROP TABLE "Nip05";
-- CreateTable
CREATE TABLE "PlatformNip05" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"pubkey" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PlatformNip05_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PlatformLightningAddress" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"allowsNostr" BOOLEAN NOT NULL DEFAULT true,
"description" TEXT,
"maxSendable" INTEGER NOT NULL DEFAULT 10000000,
"minSendable" INTEGER NOT NULL DEFAULT 1,
"invoiceMacaroon" TEXT NOT NULL,
"lndCert" TEXT,
"lndHost" TEXT NOT NULL,
"lndPort" TEXT NOT NULL DEFAULT '8080',
CONSTRAINT "PlatformLightningAddress_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PlatformNip05_userId_key" ON "PlatformNip05"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "PlatformLightningAddress_userId_key" ON "PlatformLightningAddress"("userId");
-- AddForeignKey
ALTER TABLE "PlatformNip05" ADD CONSTRAINT "PlatformNip05_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PlatformLightningAddress" ADD CONSTRAINT "PlatformLightningAddress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `image` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "image",
DROP COLUMN "name";

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "PlatformLightningAddress" ALTER COLUMN "maxSendable" SET DEFAULT 10000000000,
ALTER COLUMN "maxSendable" SET DATA TYPE BIGINT,
ALTER COLUMN "minSendable" SET DEFAULT 1000,
ALTER COLUMN "minSendable" SET DATA TYPE BIGINT;

View File

@ -13,15 +13,12 @@ generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
// todo name and username?
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
pubkey String? @unique pubkey String? @unique
privkey String? privkey String?
name String?
email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
image String?
username String? @unique username String? @unique
avatar String? avatar String?
purchased Purchase[] purchased Purchase[]
@ -36,8 +33,10 @@ model User {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
userLessons UserLesson[] userLessons UserLesson[]
userCourses UserCourse[] userCourses UserCourse[]
nip05 Nip05? nip05 String?
lightningAddress LightningAddress? lud16 String?
platformNip05 PlatformNip05?
platformLightningAddress PlatformLightningAddress?
userBadges UserBadge[] userBadges UserBadge[]
} }
@ -226,7 +225,7 @@ model UserCourse {
@@unique([userId, courseId]) @@unique([userId, courseId])
} }
model Nip05 { model PlatformNip05 {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @unique userId String @unique
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
@ -236,16 +235,15 @@ model Nip05 {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model LightningAddress { model PlatformLightningAddress {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @unique userId String @unique
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
name String name String
allowsNostr Boolean @default(true) allowsNostr Boolean @default(true)
description String? description String?
// todo: change to BigInt to support native millisats maxSendable BigInt @default(10000000000)
maxSendable Int @default(10000000) minSendable BigInt @default(1000)
minSendable Int @default(1)
invoiceMacaroon String invoiceMacaroon String
lndCert String? lndCert String?
lndHost String lndHost String

View File

@ -213,7 +213,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
<p className='text-lg text-white'> <p className='text-lg text-white'>
By{' '} By{' '}
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'> <a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} {lesson.author?.username || lesson.author?.pubkey}
</a> </a>
</p> </p>
</div> </div>

View File

@ -86,7 +86,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
<p className='text-lg'> <p className='text-lg'>
Created by{' '} Created by{' '}
<a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'> <a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} {lesson.author?.username || lesson.author?.pubkey}
</a> </a>
</p> </p>
</div> </div>

View File

@ -127,7 +127,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
<p className='text-lg text-white'> <p className='text-lg text-white'>
By{' '} By{' '}
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'> <a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} {lesson.author?.username || lesson.author?.pubkey}
</a> </a>
</p> </p>
</div> </div>

View File

@ -78,7 +78,7 @@ const DraftCourseLesson = ({ lesson, course }) => {
<p className='text-lg'> <p className='text-lg'>
Created by{' '} Created by{' '}
<a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'> <a rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} {lesson.author?.username || lesson.author?.pubkey}
</a> </a>
</p> </p>
</div> </div>

View File

@ -189,7 +189,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
<p className='text-lg text-white'> <p className='text-lg text-white'>
Created by{' '} Created by{' '}
<a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'> <a rel='noreferrer noopener' target='_blank' className='text-blue-300 hover:underline'>
{lesson.author?.username || lesson.author?.name || lesson.author?.pubkey} {lesson.author?.username || lesson.author?.pubkey}
</a> </a>
</p> </p>
</div> </div>

View File

@ -56,7 +56,7 @@ const UserAvatar = () => {
return null; // Or return a loader/spinner/placeholder return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) { } else if (user && Object.keys(user).length > 0) {
// User exists, show username or pubkey // User exists, show username or pubkey
const displayName = user.username || user?.name || user?.email || user?.pubkey.slice(0, 10) + '...' || "Anon"; const displayName = user?.username || user?.email || user?.pubkey?.slice(0, 10) + '...' || "Anon";
const items = [ const items = [
{ {

View File

@ -79,6 +79,9 @@ const LinkAccountsCard = ({ session }) => {
}); });
if (response.ok) { if (response.ok) {
// clear the local storage of the users ephemeral keys
localStorage.removeItem('anonymousPrivkey');
localStorage.removeItem('anonymousPubkey');
showToast('success', 'Success', 'Nostr account linked successfully'); showToast('success', 'Success', 'Nostr account linked successfully');
// Refresh the session to get updated user data // Refresh the session to get updated user data
await update(); await update();
@ -181,7 +184,7 @@ const LinkAccountsCard = ({ session }) => {
icon="pi pi-github" icon="pi pi-github"
onClick={handleGithubLink} onClick={handleGithubLink}
disabled={isGithubLinked} disabled={isGithubLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto`} className={`text-[#f8f8ff] w-[250px]`}
rounded rounded
/> />
@ -190,7 +193,7 @@ const LinkAccountsCard = ({ session }) => {
icon={<Image src="/images/nostr-icon-white.png" width={20} height={20} alt="Nostr" className="mr-2" />} icon={<Image src="/images/nostr-icon-white.png" width={20} height={20} alt="Nostr" className="mr-2" />}
onClick={handleNostrLink} onClick={handleNostrLink}
disabled={isNostrLinked} disabled={isNostrLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto flex items-center justify-center`} className={`text-[#f8f8ff] w-[250px]`}
rounded rounded
/> />
@ -199,7 +202,7 @@ const LinkAccountsCard = ({ session }) => {
icon="pi pi-envelope" icon="pi pi-envelope"
onClick={handleEmailLink} onClick={handleEmailLink}
disabled={isEmailLinked} disabled={isEmailLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto`} className={`text-[#f8f8ff] w-[250px]`}
rounded rounded
/> />
</div> </div>

View File

@ -70,7 +70,7 @@ const UserProfileCard = ({ user }) => {
className="rounded-full m-2 mt-0 object-cover max-w-[100px] max-h-[100px]" className="rounded-full m-2 mt-0 object-cover max-w-[100px] max-h-[100px]"
/> />
<h3 className="text-center"> <h3 className="text-center">
{user.username || user?.name || user?.email || "Anon"} {user.username || user?.email || "Anon"}
</h3> </h3>
<div className="flex flex-col gap-2 justify-center w-full overflow-hidden"> <div className="flex flex-col gap-2 justify-center w-full overflow-hidden">
{ {
@ -98,13 +98,17 @@ const UserProfileCard = ({ user }) => {
</div> </div>
<div className='w-full flex flex-row justify-between'> <div className='w-full flex flex-row justify-between'>
<div className="flex flex-col justify-between gap-4 my-2"> <div className="flex flex-col justify-between gap-4 my-2">
{user?.lightningAddress ? ( {user?.platformLightningAddress ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">Lightning Address:</span> {user.lightningAddress.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.lightningAddress.name + "@plebdevs.com")} /> <span className="font-bold">Lightning Address:</span> {user.platformLightningAddress.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.platformLightningAddress.name + "@plebdevs.com")} />
</h4>
) : user?.lud16 ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">Lightning Address:</span> {user.lud16} <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.lud16)} />
</h4> </h4>
) : ( ) : (
<div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<h4 > <h4>
<span className="font-bold">Lightning Address:</span> None <span className="font-bold">Lightning Address:</span> None
</h4> </h4>
<MoreInfo <MoreInfo
@ -115,9 +119,23 @@ const UserProfileCard = ({ user }) => {
/> />
</div> </div>
)} )}
{user?.nip05 ? ( {user?.platformNip05 ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">NIP-05:</span> {user.nip05.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.nip05.name + "@plebdevs.com")} /> <span className="font-bold">NIP-05:</span>{' '}
{user.platformNip05.name}@plebdevs.com{' '}
<i
className="pi pi-copy cursor-pointer hover:text-gray-400"
onClick={() => copyToClipboard(`${user.platformNip05.name}@plebdevs.com`)}
/>
</h4>
) : user?.nip05 ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">NIP-05:</span>{' '}
{user.nip05}{' '}
<i
className="pi pi-copy cursor-pointer hover:text-gray-400"
onClick={() => copyToClipboard(user.nip05)}
/>
</h4> </h4>
) : ( ) : (
<div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
@ -125,9 +143,9 @@ const UserProfileCard = ({ user }) => {
<span className="font-bold">NIP-05:</span> None <span className="font-bold">NIP-05:</span> None
</h4> </h4>
<MoreInfo <MoreInfo
tooltip="PlebDevs Custom NIP-05" tooltip="NIP-05 Info"
modalTitle="PlebDevs Custom NIP-05" modalTitle="What is NIP-05?"
modalBody="This is a placeholder for your PlebDevs issued NIP-05 (claimable through subscription)" modalBody="NIP-05 is a verification standard in Nostr that links your identity to a domain name, similar to how Twitter verifies accounts. It helps prove ownership of your identity."
className="text-xs" className="text-xs"
/> />
</div> </div>
@ -169,7 +187,7 @@ const UserProfileCard = ({ user }) => {
/> />
</div> </div>
<h3 className="self-start"> <h3 className="self-start">
{user.username || user?.name || user?.email || "Anon"} {user.username || user?.email || "Anon"}
</h3> </h3>
{ {
user?.pubkey && ( user?.pubkey && (
@ -193,10 +211,14 @@ const UserProfileCard = ({ user }) => {
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col justify-between gap-4 my-2"> <div className="flex flex-col justify-between gap-2">
{user?.lightningAddress ? ( {user?.platformLightningAddress ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <h4 className="bg-gray-900 rounded-lg p-2 max-lap:w-fit min-w-[240px]">
<span className="font-bold">Lightning Address:</span> {user.lightningAddress.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.lightningAddress.name + "@plebdevs.com")} /> <span className="font-bold">Lightning Address:</span> {user.platformLightningAddress.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.platformLightningAddress.name + "@plebdevs.com")} />
</h4>
) : user?.lud16 ? (
<h4 className="bg-gray-900 rounded-lg p-2 max-lap:w-fit min-w-[240px]">
<span className="font-bold">Lightning Address:</span> {user.lud16} <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.lud16)} />
</h4> </h4>
) : ( ) : (
<div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
@ -211,9 +233,23 @@ const UserProfileCard = ({ user }) => {
/> />
</div> </div>
)} )}
{user?.nip05 ? ( {user?.platformNip05 ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">NIP-05:</span> {user.nip05.name}@plebdevs.com <i className="pi pi-copy cursor-pointer hover:text-gray-400" onClick={() => copyToClipboard(user.nip05.name + "@plebdevs.com")} /> <span className="font-bold">NIP-05:</span>{' '}
{user.platformNip05.name}@plebdevs.com{' '}
<i
className="pi pi-copy cursor-pointer hover:text-gray-400"
onClick={() => copyToClipboard(`${user.platformNip05.name}@plebdevs.com`)}
/>
</h4>
) : user?.nip05 ? (
<h4 className="bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">
<span className="font-bold">NIP-05:</span>{' '}
{user.nip05}{' '}
<i
className="pi pi-copy cursor-pointer hover:text-gray-400"
onClick={() => copyToClipboard(user.nip05)}
/>
</h4> </h4>
) : ( ) : (
<div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]"> <div className="flex flex-row justify-between bg-gray-900 rounded-lg p-3 max-lap:w-fit min-w-[240px]">

View File

@ -14,8 +14,8 @@ const LightningAddressForm = ({ visible, onHide }) => {
const [existingLightningAddress, setExistingLightningAddress] = useState(null); const [existingLightningAddress, setExistingLightningAddress] = useState(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [maxSendable, setMaxSendable] = useState(10000000); const [maxSendable, setMaxSendable] = useState(10000000000);
const [minSendable, setMinSendable] = useState(1); const [minSendable, setMinSendable] = useState(1000);
const [invoiceMacaroon, setInvoiceMacaroon] = useState(''); const [invoiceMacaroon, setInvoiceMacaroon] = useState('');
const [lndCert, setLndCert] = useState(''); const [lndCert, setLndCert] = useState('');
const [lndHost, setLndHost] = useState(''); const [lndHost, setLndHost] = useState('');
@ -26,18 +26,18 @@ const LightningAddressForm = ({ visible, onHide }) => {
const { showToast } = useToast(); const { showToast } = useToast();
useEffect(() => { useEffect(() => {
if (session && session?.user && !session?.user?.lightningAddress) { if (session && session?.user && !session?.user?.platformLightningAddress) {
setName(session.user.name || ''); setName(session.user.username || '');
} else if (session && session?.user && session?.user?.lightningAddress) { } else if (session && session?.user && session?.user?.platformLightningAddress) {
setExistingLightningAddress(session.user.lightningAddress); setExistingLightningAddress(session.user.platformLightningAddress);
setName(session.user.lightningAddress.name || ''); setName(session.user.platformLightningAddress.name || '');
setDescription(session.user.lightningAddress.description || ''); setDescription(session.user.platformLightningAddress.description || '');
setMaxSendable(session.user.lightningAddress.maxSendable || 10000000); setMaxSendable(Number(session.user.platformLightningAddress.maxSendable) || 10000000000);
setMinSendable(session.user.lightningAddress.minSendable || 1); setMinSendable(Number(session.user.platformLightningAddress.minSendable) || 1000);
setInvoiceMacaroon(session.user.lightningAddress.invoiceMacaroon || ''); setInvoiceMacaroon(session.user.platformLightningAddress.invoiceMacaroon || '');
setLndCert(session.user.lightningAddress.lndCert || ''); setLndCert(session.user.platformLightningAddress.lndCert || '');
setLndHost(session.user.lightningAddress.lndHost || ''); setLndHost(session.user.platformLightningAddress.lndHost || '');
setLndPort(session.user.lightningAddress.lndPort || '8080'); setLndPort(session.user.platformLightningAddress.lndPort || '8080');
} }
}, [session]); }, [session]);
@ -46,10 +46,21 @@ const LightningAddressForm = ({ visible, onHide }) => {
try { try {
let response; let response;
const lowercaseName = name.toLowerCase(); const lowercaseName = name.toLowerCase();
const data = {
name: lowercaseName,
description,
maxSendable: BigInt(maxSendable).toString(),
minSendable: BigInt(minSendable).toString(),
invoiceMacaroon,
lndCert,
lndHost,
lndPort
};
if (existingLightningAddress) { if (existingLightningAddress) {
response = await axios.put(`/api/users/${session.user.id}/lightning-address`, { name: lowercaseName, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort }); response = await axios.put(`/api/users/${session.user.id}/lightning-address`, data);
} else { } else {
response = await axios.post(`/api/users/${session.user.id}/lightning-address`, { name: lowercaseName, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort }); response = await axios.post(`/api/users/${session.user.id}/lightning-address`, data);
} }
if (!existingLightningAddress && response.status === 201) { if (!existingLightningAddress && response.status === 201) {
showToast('success', 'Lightning Address Claimed', 'Your Lightning Address has been claimed'); showToast('success', 'Lightning Address Claimed', 'Your Lightning Address has been claimed');
@ -101,10 +112,9 @@ const LightningAddressForm = ({ visible, onHide }) => {
<label>Description</label> <label>Description</label>
<InputText placeholder="Description" value={description} onChange={(e) => setDescription(e.target.value)} tooltip='This is your Lightning Address description, it will be displayed as the description LUD16 lnurlp endpoint' /> <InputText placeholder="Description" value={description} onChange={(e) => setDescription(e.target.value)} tooltip='This is your Lightning Address description, it will be displayed as the description LUD16 lnurlp endpoint' />
<label>Max Sendable</label> <label>Max Sendable</label>
{/* Todo: max is 2,147,483 sats until i imlement bigInt for sat amounts */} <InputNumber placeholder="Max Sendable" value={maxSendable} onChange={(e) => setMaxSendable(e.value)} min={1000} tooltip='Maximum amount in millisats (1000 millisats = 1 sat)' />
<InputNumber placeholder="Max Sendable" value={maxSendable} onChange={(e) => setMaxSendable(e.target.value)} max={2147483647} min={1000} tooltip='This is the maximum amount of sats that can be sent to your Lightning Address (currently denominated in sats NOT msat)' />
<label>Min Sendable</label> <label>Min Sendable</label>
<InputNumber placeholder="Min Sendable" value={minSendable} onChange={(e) => setMinSendable(e.target.value)} min={1} max={2147483647} tooltip='This is the minimum amount of sats that can be sent to your Lightning Address (currently denominated in sats NOT msat)' /> <InputNumber placeholder="Min Sendable" value={minSendable} onChange={(e) => setMinSendable(e.value)} min={1} tooltip='Minimum amount in millisats (1000 millisats = 1 sat)' />
<label>Invoice Macaroon</label> <label>Invoice Macaroon</label>
<InputText placeholder="Invoice Macaroon" value={invoiceMacaroon} onChange={(e) => setInvoiceMacaroon(e.target.value)} tooltip='This is your LND Invoice Macaroon, it is used to create invoices for your Lightning Address but DOES NOT grant access to move funds from your LND node' /> <InputText placeholder="Invoice Macaroon" value={invoiceMacaroon} onChange={(e) => setInvoiceMacaroon(e.target.value)} tooltip='This is your LND Invoice Macaroon, it is used to create invoices for your Lightning Address but DOES NOT grant access to move funds from your LND node' />
<label>LND Cert</label> <label>LND Cert</label>

View File

@ -19,13 +19,13 @@ const Nip05Form = ({ visible, onHide }) => {
const { showToast } = useToast(); const { showToast } = useToast();
useEffect(() => { useEffect(() => {
if (session && session?.user && !session?.user?.nip05) { if (session && session?.user && !session?.user?.platformNip05) {
setPubkey(session.user.pubkey || ''); setPubkey(session.user.pubkey || '');
setName(session.user.name || ''); setName(session.user.username || '');
} else if (session && session?.user && session?.user?.nip05) { } else if (session && session?.user && session?.user?.platformNip05) {
setExistingNip05(session.user.nip05); setExistingNip05(session.user.platformNip05);
setPubkey(session.user.nip05.pubkey || ''); setPubkey(session.user.platformNip05.pubkey || '');
setName(session.user.nip05.name || ''); setName(session.user.platformNip05.name || '');
} }
}, [session]); }, [session]);

View File

@ -7,7 +7,6 @@ import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { Card } from 'primereact/card'; import { Card } from 'primereact/card';
import { Badge } from 'primereact/badge';
import GenericButton from '@/components/buttons/GenericButton'; import GenericButton from '@/components/buttons/GenericButton';
import { Menu } from "primereact/menu"; import { Menu } from "primereact/menu";
import { Message } from "primereact/message"; import { Message } from "primereact/message";
@ -104,14 +103,14 @@ const SubscribeModal = ({ user }) => {
}, },
}, },
{ {
label: session?.user?.lightningAddress ? "Update PlebDevs Lightning Address" : "Claim PlebDevs Lightning Address", label: session?.user?.platformLightningAddress ? "Update PlebDevs Lightning Address" : "Claim PlebDevs Lightning Address",
icon: "pi pi-bolt", icon: "pi pi-bolt",
command: () => { command: () => {
setLightningAddressVisible(true); setLightningAddressVisible(true);
}, },
}, },
{ {
label: session?.user?.nip05 ? "Update PlebDevs Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05", label: session?.user?.platformNip05?.name ? "Update PlebDevs Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05",
icon: "pi pi-at", icon: "pi pi-at",
command: () => { command: () => {
setNip05Visible(true); setNip05Visible(true);
@ -152,14 +151,14 @@ const SubscribeModal = ({ user }) => {
{subscribed && !user?.role?.nwc && ( {subscribed && !user?.role?.nwc && (
<div className="flex flex-col"> <div className="flex flex-col">
<Message className="w-fit" severity="success" text="Subscribed!" /> <Message className="w-fit" severity="success" text="Subscribed!" />
<p className="mt-4">Thank you for your support 🎉</p> <p className="mt-3">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p> <p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p>
</div> </div>
)} )}
{subscribed && user?.role?.nwc && ( {subscribed && user?.role?.nwc && (
<div className="flex flex-col"> <div className="flex flex-col">
<Message className="w-fit" severity="success" text="Subscribed!" /> <Message className="w-fit" severity="success" text="Subscribed!" />
<p className="mt-4">Thank you for your support 🎉</p> <p className="mt-3">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Recurring subscription will AUTO renew on {subscribedUntil.toLocaleDateString()}</p> <p className="text-sm text-gray-400">Recurring subscription will AUTO renew on {subscribedUntil.toLocaleDateString()}</p>
</div> </div>
)} )}
@ -168,7 +167,7 @@ const SubscribeModal = ({ user }) => {
<Message className="w-fit" severity="info" text="You currently have no active subscription" /> <Message className="w-fit" severity="info" text="You currently have no active subscription" />
<GenericButton <GenericButton
label="Subscribe" label="Subscribe"
className="w-auto mt-4 text-[#f8f8ff]" className="w-auto mt-3 text-[#f8f8ff]"
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
/> />
</div> </div>
@ -232,7 +231,7 @@ const SubscribeModal = ({ user }) => {
visible={calendlyVisible} visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)} onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id} userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username} userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email} userEmail={session?.user?.email}
/> />
<CancelSubscription <CancelSubscription

View File

@ -1,6 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import axios from 'axios'; import axios from 'axios';
import { Card } from 'primereact/card'; import { Card } from 'primereact/card';
@ -21,9 +20,7 @@ import RenewSubscription from '@/components/profile/subscription/RenewSubscripti
const UserSubscription = () => { const UserSubscription = () => {
const { data: session, update } = useSession(); const { data: session, update } = useSession();
const { showToast } = useToast(); const { showToast } = useToast();
const router = useRouter();
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
const menu = useRef(null);
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [subscribed, setSubscribed] = useState(false); const [subscribed, setSubscribed] = useState(false);
@ -178,8 +175,8 @@ const UserSubscription = () => {
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<GenericButton severity="info" outlined className="w-fit text-start" label="Schedule 1:1" icon="pi pi-calendar" onClick={() => setCalendlyVisible(true)} /> <GenericButton severity="info" outlined className="w-fit text-start" label="Schedule 1:1" icon="pi pi-calendar" onClick={() => setCalendlyVisible(true)} />
<GenericButton severity="help" outlined className="w-fit text-start" label={user?.nip05 ? "Update Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05"} icon="pi pi-at" onClick={() => setNip05Visible(true)} /> <GenericButton severity="help" outlined className="w-fit text-start" label={user?.platformNip05?.name ? "Update Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05"} icon="pi pi-at" onClick={() => setNip05Visible(true)} />
<GenericButton severity="warning" outlined className="w-fit text-start" label={user?.lightningAddress ? "Update Lightning Address" : "Claim PlebDevs Lightning Address"} icon={<i style={{ color: "orange" }} className="pi pi-bolt mr-2"></i>} onClick={() => setLightningAddressVisible(true)} /> <GenericButton severity="warning" outlined className="w-fit text-start" label={user?.platformLightningAddress ? "Update Lightning Address" : "Claim PlebDevs Lightning Address"} icon={<i style={{ color: "orange" }} className="pi pi-bolt mr-2"></i>} onClick={() => setLightningAddressVisible(true)} />
</div> </div>
</div> </div>
)} )}
@ -225,7 +222,7 @@ const UserSubscription = () => {
visible={calendlyVisible} visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)} onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id} userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username} userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email} userEmail={session?.user?.email}
/> />
<CancelSubscription <CancelSubscription

View File

@ -1,24 +1,24 @@
import prisma from "@/db/prisma"; import prisma from "@/db/prisma";
export const getAllLightningAddresses = async () => { export const getAllLightningAddresses = async () => {
return await prisma.lightningAddress.findMany(); return await prisma.platformLightningAddress.findMany();
}; };
export const getLightningAddressByName = async (name) => { export const getLightningAddressByName = async (name) => {
return await prisma.lightningAddress.findFirst({ return await prisma.platformLightningAddress.findFirst({
where: { name }, where: { name },
}); });
}; };
export const getLightningAddress = async (userId) => { export const getLightningAddress = async (userId) => {
return await prisma.lightningAddress.findUnique({ return await prisma.platformLightningAddress.findUnique({
where: { userId }, where: { userId },
}); });
}; };
export const createLightningAddress = async (userId, name, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort) => { export const createLightningAddress = async (userId, name, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort) => {
try { try {
return await prisma.lightningAddress.create({ return await prisma.platformLightningAddress.create({
data: { data: {
userId, userId,
name, name,
@ -38,14 +38,14 @@ export const createLightningAddress = async (userId, name, description, maxSenda
}; };
export const updateLightningAddress = async (userId, data) => { export const updateLightningAddress = async (userId, data) => {
return await prisma.lightningAddress.update({ return await prisma.platformLightningAddress.update({
where: { userId }, where: { userId },
data, data,
}); });
}; };
export const deleteLightningAddress = async (userId) => { export const deleteLightningAddress = async (userId) => {
return await prisma.lightningAddress.delete({ return await prisma.platformLightningAddress.delete({
where: { userId }, where: { userId },
}); });
}; };

View File

@ -1,36 +1,36 @@
import prisma from "@/db/prisma"; import prisma from "@/db/prisma";
export const getAllNip05s = async () => { export const getAllNip05s = async () => {
return await prisma.nip05.findMany(); return await prisma.platformNip05.findMany();
}; };
export const getNip05ByName = async (name) => { export const getNip05ByName = async (name) => {
return await prisma.nip05.findFirst({ return await prisma.platformNip05.findFirst({
where: { name }, where: { name },
}); });
}; };
export const getNip05 = async (userId) => { export const getNip05 = async (userId) => {
return await prisma.nip05.findUnique({ return await prisma.platformNip05.findUnique({
where: { userId }, where: { userId },
}); });
}; };
export const createNip05 = async (userId, pubkey, name) => { export const createNip05 = async (userId, pubkey, name) => {
return await prisma.nip05.create({ return await prisma.platformNip05.create({
data: { userId, pubkey, name }, data: { userId, pubkey, name },
}); });
}; };
export const updateNip05 = async (userId, data) => { export const updateNip05 = async (userId, data) => {
return await prisma.nip05.update({ return await prisma.platformNip05.update({
where: { userId }, where: { userId },
data, data,
}); });
}; };
export const deleteNip05 = async (userId) => { export const deleteNip05 = async (userId) => {
return await prisma.nip05.delete({ return await prisma.platformNip05.delete({
where: { userId }, where: { userId },
}); });
}; };

View File

@ -50,8 +50,8 @@ export const getUserById = async (id) => {
lesson: true, lesson: true,
}, },
}, },
nip05: true, platformNip05: true,
lightningAddress: true, platformLightningAddress: true,
userBadges: { userBadges: {
include: { include: {
badge: true badge: true
@ -82,8 +82,8 @@ export const getUserByPubkey = async (pubkey) => {
lesson: true, lesson: true,
}, },
}, },
nip05: true, platformNip05: true,
lightningAddress: true, platformLightningAddress: true,
userBadges: { userBadges: {
include: { include: {
badge: true badge: true
@ -278,8 +278,8 @@ export const getUserByEmail = async (email) => {
lesson: true, lesson: true,
}, },
}, },
nip05: true, platformNip05: true,
lightningAddress: true, platformLightningAddress: true,
userBadges: { userBadges: {
include: { include: {
badge: true badge: true

View File

@ -33,14 +33,19 @@ const syncNostrProfile = async (pubkey) => {
let dbUser = await getUserByPubkey(pubkey); let dbUser = await getUserByPubkey(pubkey);
if (dbUser) { if (dbUser) {
// Update existing user if kind0 fields differ // Update existing user if any of the kind0 fields differ
if (fields.avatar !== dbUser.avatar || fields.username !== dbUser.username) { if (fields.avatar !== dbUser.avatar ||
fields.username !== dbUser.username ||
fields.lud16 !== dbUser.lud16 ||
fields.nip05 !== dbUser.nip05) {
const updates = { const updates = {
...(fields.avatar !== dbUser.avatar && { avatar: fields.avatar }), ...(fields.avatar !== dbUser.avatar && { avatar: fields.avatar }),
...(fields.username !== dbUser.username && { ...(fields.username !== dbUser.username && {
username: fields.username, username: fields.username
name: fields.username }),
}) ...(fields.lud16 !== dbUser.lud16 && { lud16: fields.lud16 }),
...(fields.nip05 !== dbUser.nip05 && { nip05: fields.nip05 })
}; };
await updateUser(dbUser.id, updates); await updateUser(dbUser.id, updates);
dbUser = await getUserByPubkey(pubkey); dbUser = await getUserByPubkey(pubkey);
@ -48,11 +53,14 @@ const syncNostrProfile = async (pubkey) => {
} else { } else {
// Create new user // Create new user
const username = fields.username || pubkey.slice(0, 8); const username = fields.username || pubkey.slice(0, 8);
const lud16 = fields.lud16 || null;
const nip05 = fields.nip05 || null;
const payload = { const payload = {
pubkey, pubkey,
username, username,
avatar: fields.avatar, avatar: fields.avatar,
name: username lud16,
nip05
}; };
dbUser = await createUser(payload); dbUser = await createUser(payload);
@ -126,8 +134,7 @@ export const authOptions = {
if (!user) { if (!user) {
user = await createUser({ user = await createUser({
...keys, ...keys,
username: `anon-${keys.pubkey.slice(0, 8)}`, username: `anon-${keys.pubkey.slice(0, 8)}`
name: `anon-${keys.pubkey.slice(0, 8)}`
}); });
} }
return { ...user, privkey: keys.privkey }; return { ...user, privkey: keys.privkey };
@ -157,7 +164,7 @@ export const authOptions = {
id: profile.id.toString(), id: profile.id.toString(),
pubkey: keys.pubkey, pubkey: keys.pubkey,
privkey: keys.privkey, privkey: keys.privkey,
name: profile.login, username: profile.login,
email: profile.email, email: profile.email,
avatar: profile.avatar_url avatar: profile.avatar_url
}; };
@ -194,8 +201,8 @@ export const authOptions = {
purchased: true, purchased: true,
userCourses: true, userCourses: true,
userLessons: true, userLessons: true,
nip05: true, platformNip05: true,
lightningAddress: true, platformLightningAddress: true,
userBadges: true userBadges: true
} }
}); });
@ -232,7 +239,6 @@ export const authOptions = {
username: user.email.split('@')[0], username: user.email.split('@')[0],
email: user.email, email: user.email,
avatar: user.image, avatar: user.image,
name: user.email.split('@')[0],
} }
// Update the user with the new keypair // Update the user with the new keypair
@ -256,7 +262,15 @@ export const authOptions = {
if (userData) { if (userData) {
const fullUser = await getUserById(userData.id); const fullUser = await getUserById(userData.id);
// Convert BigInt values to strings if they exist
if (fullUser.platformLightningAddress) {
fullUser.platformLightningAddress = {
...fullUser.platformLightningAddress,
maxSendable: fullUser.platformLightningAddress.maxSendable?.toString(),
minSendable: fullUser.platformLightningAddress.minSendable?.toString()
};
}
// Get the user's GitHub account if it exists // Get the user's GitHub account if it exists
const githubAccount = await prisma.account.findFirst({ const githubAccount = await prisma.account.findFirst({
where: { where: {
@ -273,13 +287,14 @@ export const authOptions = {
role: fullUser.role, role: fullUser.role,
username: fullUser.username, username: fullUser.username,
avatar: fullUser.avatar, avatar: fullUser.avatar,
name: fullUser.name,
email: fullUser.email, email: fullUser.email,
userCourses: fullUser.userCourses, userCourses: fullUser.userCourses,
userLessons: fullUser.userLessons, userLessons: fullUser.userLessons,
purchased: fullUser.purchased, purchased: fullUser.purchased,
nip05: fullUser.nip05, nip05: fullUser.nip05,
lightningAddress: fullUser.lightningAddress, lud16: fullUser.lud16,
platformNip05: fullUser.platformNip05,
platformLightningAddress: fullUser.platformLightningAddress,
githubUsername: token.githubUsername, githubUsername: token.githubUsername,
createdAt: fullUser.createdAt, createdAt: fullUser.createdAt,
userBadges: fullUser.userBadges userBadges: fullUser.userBadges
@ -300,15 +315,22 @@ export const authOptions = {
return session; return session;
}, },
async jwt({ token, user, account, profile, session }) { async jwt({ token, user, account, profile, session }) {
// Convert BigInt values to strings if they exist
if (user?.platformLightningAddress) {
user.platformLightningAddress = {
...user.platformLightningAddress,
maxSendable: user.platformLightningAddress.maxSendable?.toString(),
minSendable: user.platformLightningAddress.minSendable?.toString()
};
}
// If we are linking a github account to an existing email or anon account (we have privkey) // If we are linking a github account to an existing email or anon account (we have privkey)
if (account?.provider === "github" && user?.id && user?.pubkey && user?.privkey) { if (account?.provider === "github" && user?.id && user?.pubkey && user?.privkey) {
try { try {
// First update the user's profile with GitHub info // First update the user's profile with GitHub info
const updatedUser = await updateUser(user.id, { const updatedUser = await updateUser(user.id, {
name: profile?.login || profile?.name,
username: profile?.login || profile?.name, username: profile?.login || profile?.name,
avatar: profile?.avatar_url, avatar: profile?.avatar_url,
image: profile?.avatar_url,
}); });
// Get the updated user // Get the updated user
@ -341,10 +363,8 @@ export const authOptions = {
if (!existingGithubAccount) { if (!existingGithubAccount) {
// Update user profile with GitHub info // Update user profile with GitHub info
const updatedUser = await updateUser(user.id, { const updatedUser = await updateUser(user.id, {
name: profile?.login || profile?.name,
username: profile?.login || profile?.name, username: profile?.login || profile?.name,
avatar: profile?.avatar_url, avatar: profile?.avatar_url,
image: profile?.avatar_url,
email: profile?.email // Add email if user wants it email: profile?.email // Add email if user wants it
}); });

View File

@ -60,16 +60,21 @@ export default async function handler(req, res) {
descriptionHash = Buffer.from(hash, 'hex').toString('base64'); descriptionHash = Buffer.from(hash, 'hex').toString('base64');
} }
// Convert amount from millisatoshis to satoshis // Check amount against BigInt min/max values
if (amount < (foundAddress.minSendable)) { if (amount < foundAddress.minSendable) {
res.status(400).json({ error: 'Amount too low' }); res.status(400).json({ error: 'Amount too low' });
return; return;
} else if (amount > (foundAddress.maxSendable || Number.MAX_SAFE_INTEGER)) { } else if (amount > foundAddress.maxSendable) {
res.status(400).json({ error: 'Amount too high' }); res.status(400).json({ error: 'Amount too high' });
return; return;
} else { } else {
try { try {
const response = await axios.post(`${BACKEND_URL}/api/lightning-address/lnd`, { amount: amount, description_hash: descriptionHash, name: slug, zap_request: queryParams?.nostr ? queryParams.nostr : null }, { const response = await axios.post(`${BACKEND_URL}/api/lightning-address/lnd`, {
amount: amount,
description_hash: descriptionHash,
name: slug,
zap_request: queryParams?.nostr ? queryParams.nostr : null
}, {
headers: { headers: {
'Authorization': PLEBDEVS_API_KEY 'Authorization': PLEBDEVS_API_KEY
} }

View File

@ -35,8 +35,8 @@ export default async function handler(req, res) {
res.status(200).json({ res.status(200).json({
callback: `${BACKEND_URL}/api/lightning-address/callback/${foundAddress.name}`, callback: `${BACKEND_URL}/api/lightning-address/callback/${foundAddress.name}`,
maxSendable: foundAddress.maxSendable || 10000000000, maxSendable: parseInt(foundAddress.maxSendable),
minSendable: foundAddress.minSendable || 1000, minSendable: parseInt(foundAddress.minSendable),
metadata: JSON.stringify(metadata), metadata: JSON.stringify(metadata),
tag: 'payRequest', tag: 'payRequest',
allowsNostr: true, allowsNostr: true,

View File

@ -30,9 +30,23 @@ export default async function handler(req, res) {
case 'POST': case 'POST':
try { try {
const { name, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort } = req.body; const { name, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort } = req.body;
const lightningAddress = await createLightningAddress(userId, name, description, maxSendable, minSendable, invoiceMacaroon, lndCert, lndHost, lndPort); const lightningAddress = await createLightningAddress(
userId,
name,
description,
BigInt(maxSendable),
BigInt(minSendable),
invoiceMacaroon,
lndCert,
lndHost,
lndPort
);
res.status(201).json(lightningAddress); res.status(201).json({
...lightningAddress,
maxSendable: lightningAddress.maxSendable.toString(),
minSendable: lightningAddress.minSendable.toString()
});
} catch (error) { } catch (error) {
console.error('Error creating Lightning Address:', error); console.error('Error creating Lightning Address:', error);
res.status(500).json({ error: 'Error creating Lightning Address', errorMessage: error.message }); res.status(500).json({ error: 'Error creating Lightning Address', errorMessage: error.message });

View File

@ -17,7 +17,7 @@ export default function SignIn() {
const handleEmailSignIn = async (e) => { const handleEmailSignIn = async (e) => {
e.preventDefault() e.preventDefault()
await signIn("email", { email, callbackUrl: '/' }) await signIn("email", { email, callbackUrl: '/profile' })
} }
const handleNostrSignIn = async (e) => { const handleNostrSignIn = async (e) => {
@ -28,7 +28,7 @@ export default function SignIn() {
try { try {
const user = await ndk.signer.user() const user = await ndk.signer.user()
const pubkey = user?._pubkey const pubkey = user?._pubkey
signIn("nostr", { pubkey }) signIn("nostr", { pubkey, callbackUrl: '/profile' })
} catch (error) { } catch (error) {
console.error("Error signing Nostr event:", error) console.error("Error signing Nostr event:", error)
} }
@ -46,7 +46,7 @@ export default function SignIn() {
pubkey: storedPubkey, pubkey: storedPubkey,
privkey: storedPrivkey, privkey: storedPrivkey,
redirect: false, redirect: false,
callbackUrl: '/' callbackUrl: '/profile'
}); });
if (result?.ok) { if (result?.ok) {
@ -59,7 +59,7 @@ export default function SignIn() {
if (session?.user?.pubkey && session?.user?.privkey) { if (session?.user?.pubkey && session?.user?.privkey) {
localStorage.setItem('anonymousPubkey', session.user.pubkey); localStorage.setItem('anonymousPubkey', session.user.pubkey);
localStorage.setItem('anonymousPrivkey', session.user.privkey); localStorage.setItem('anonymousPrivkey', session.user.privkey);
router.push('/'); router.push('/profile');
} else { } else {
console.error("Session data incomplete:", session); console.error("Session data incomplete:", session);
} }
@ -77,11 +77,11 @@ export default function SignIn() {
const result = await signIn("recovery", { const result = await signIn("recovery", {
nsec, nsec,
redirect: false, redirect: false,
callbackUrl: '/' callbackUrl: '/profile'
}); });
if (result?.ok) { if (result?.ok) {
router.push('/'); router.push('/profile');
} else { } else {
console.error("Recovery login failed:", result?.error); console.error("Recovery login failed:", result?.error);
} }
@ -93,7 +93,7 @@ export default function SignIn() {
useEffect(() => { useEffect(() => {
// Redirect if already signed in // Redirect if already signed in
if (session?.user) { if (session?.user) {
router.push('/'); router.push('/profile');
} }
}, [session, router]); }, [session, router]);

View File

@ -327,7 +327,7 @@ export default function Draft() {
<p className='text-lg'> <p className='text-lg'>
Created by{' '} Created by{' '}
<a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'> <a href={`https://nostr.com/${hexToNpub(user?.pubkey)}`} rel='noreferrer noopener' target='_blank' className='text-blue-500 hover:underline'>
{user?.username || user?.name || user?.pubkey.slice(0, 10)}{'... '} {user?.username || user?.pubkey.slice(0, 10)}{'... '}
</a> </a>
</p> </p>
)} )}

View File

@ -101,12 +101,12 @@ const Subscribe = () => {
command: () => setCalendlyVisible(true), command: () => setCalendlyVisible(true),
}, },
{ {
label: session?.user?.lightningAddress ? "Update PlebDevs Lightning Address" : "Claim PlebDevs Lightning Address", label: session?.user?.platformLightningAddress ? "Update PlebDevs Lightning Address" : "Claim PlebDevs Lightning Address",
icon: "pi pi-bolt", icon: "pi pi-bolt",
command: () => setLightningAddressVisible(true), command: () => setLightningAddressVisible(true),
}, },
{ {
label: session?.user?.nip05 ? "Update PlebDevs Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05", label: session?.user?.platformNip05?.name ? "Update PlebDevs Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05",
icon: "pi pi-at", icon: "pi pi-at",
command: () => setNip05Visible(true), command: () => setNip05Visible(true),
}, },
@ -122,23 +122,6 @@ const Subscribe = () => {
}, },
]; ];
const subscriptionCardTitleAndButton = (
<div className="w-full flex flex-row justify-between items-center">
<span className="text-xl text-900 font-bold text-white">Plebdevs Subscription</span>
<i
className="pi pi-ellipsis-h text-2xlcursor-pointer hover:opacity-75"
onClick={(e) => menu.current.toggle(e)}
></i>
<Menu model={menuItems} popup ref={menu} />
</div>
);
const subscriptionCardTitle = (
<div className="w-full flex flex-row justify-between items-center">
<span className="text-xl text-900 font-bold text-white">Plebdevs Subscription</span>
</div>
);
return ( return (
<div className="p-4"> <div className="p-4">
{windowWidth < 768 && ( {windowWidth < 768 && (
@ -242,8 +225,8 @@ const Subscribe = () => {
<Card title="Subscription Benefits" className="mb-4"> <Card title="Subscription Benefits" className="mb-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<GenericButton severity="info" outlined className="w-fit text-start" label="Schedule 1:1" icon="pi pi-calendar" onClick={() => setCalendlyVisible(true)} /> <GenericButton severity="info" outlined className="w-fit text-start" label="Schedule 1:1" icon="pi pi-calendar" onClick={() => setCalendlyVisible(true)} />
<GenericButton severity="help" outlined className="w-fit text-start" label={session?.user?.nip05 ? "Update Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05"} icon="pi pi-at" onClick={() => setNip05Visible(true)} /> <GenericButton severity="help" outlined className="w-fit text-start" label={session?.user?.platformNip05?.name ? "Update Nostr NIP-05" : "Claim PlebDevs Nostr NIP-05"} icon="pi pi-at" onClick={() => setNip05Visible(true)} />
<GenericButton severity="warning" outlined className="w-fit text-start" label={session?.user?.lightningAddress ? "Update Lightning Address" : "Claim PlebDevs Lightning Address"} icon={<i style={{ color: "orange" }} className="pi pi-bolt mr-2"></i>} onClick={() => setLightningAddressVisible(true)} /> <GenericButton severity="warning" outlined className="w-fit text-start" label={session?.user?.platformLightningAddress ? "Update Lightning Address" : "Claim PlebDevs Lightning Address"} icon={<i style={{ color: "orange" }} className="pi pi-bolt mr-2"></i>} onClick={() => setLightningAddressVisible(true)} />
</div> </div>
</Card> </Card>
<Card title="Manage Subscription" className="mb-4"> <Card title="Manage Subscription" className="mb-4">
@ -296,7 +279,7 @@ const Subscribe = () => {
visible={calendlyVisible} visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)} onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id} userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username} userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email} userEmail={session?.user?.email}
/> />
<CancelSubscription <CancelSubscription

View File

@ -40,6 +40,12 @@ export const findKind0Fields = async (kind0) => {
fields.lud16 = lud16; fields.lud16 = lud16;
} }
const nip05 = findTruthyPropertyValue(kind0, ['nip05']);
if (nip05) {
fields.nip05 = nip05;
}
return fields; return fields;
} }