Merge pull request #7 from AustinKelsay/feature/refine-user-table

Refine User, Lightning Address, Nip05 tables
This commit is contained in:
Austin Kelsay 2025-02-17 12:59:37 -06:00 committed by GitHub
commit 62c4070804
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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"
}
// todo name and username?
model User {
id String @id @default(uuid())
pubkey String? @unique
privkey String?
name String?
email String? @unique
emailVerified DateTime?
image String?
username String? @unique
avatar String?
purchased Purchase[]
@ -36,8 +33,10 @@ model User {
updatedAt DateTime @updatedAt
userLessons UserLesson[]
userCourses UserCourse[]
nip05 Nip05?
lightningAddress LightningAddress?
nip05 String?
lud16 String?
platformNip05 PlatformNip05?
platformLightningAddress PlatformLightningAddress?
userBadges UserBadge[]
}
@ -226,7 +225,7 @@ model UserCourse {
@@unique([userId, courseId])
}
model Nip05 {
model PlatformNip05 {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
@ -236,16 +235,15 @@ model Nip05 {
updatedAt DateTime @updatedAt
}
model LightningAddress {
model PlatformLightningAddress {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
name String
allowsNostr Boolean @default(true)
description String?
// todo: change to BigInt to support native millisats
maxSendable Int @default(10000000)
minSendable Int @default(1)
maxSendable BigInt @default(10000000000)
minSendable BigInt @default(1000)
invoiceMacaroon String
lndCert String?
lndHost String

View File

@ -213,7 +213,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
<p className='text-lg text-white'>
By{' '}
<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>
</p>
</div>

View File

@ -86,7 +86,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
<p className='text-lg'>
Created by{' '}
<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>
</p>
</div>

View File

@ -127,7 +127,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
<p className='text-lg text-white'>
By{' '}
<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>
</p>
</div>

View File

@ -78,7 +78,7 @@ const DraftCourseLesson = ({ lesson, course }) => {
<p className='text-lg'>
Created by{' '}
<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>
</p>
</div>

View File

@ -189,7 +189,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
<p className='text-lg text-white'>
Created by{' '}
<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>
</p>
</div>

View File

@ -56,7 +56,7 @@ const UserAvatar = () => {
return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) {
// 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 = [
{

View File

@ -79,6 +79,9 @@ const LinkAccountsCard = ({ session }) => {
});
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');
// Refresh the session to get updated user data
await update();
@ -181,7 +184,7 @@ const LinkAccountsCard = ({ session }) => {
icon="pi pi-github"
onClick={handleGithubLink}
disabled={isGithubLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto`}
className={`text-[#f8f8ff] w-[250px]`}
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" />}
onClick={handleNostrLink}
disabled={isNostrLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto flex items-center justify-center`}
className={`text-[#f8f8ff] w-[250px]`}
rounded
/>
@ -199,7 +202,7 @@ const LinkAccountsCard = ({ session }) => {
icon="pi pi-envelope"
onClick={handleEmailLink}
disabled={isEmailLinked}
className={`text-[#f8f8ff] w-[250px] mx-auto`}
className={`text-[#f8f8ff] w-[250px]`}
rounded
/>
</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]"
/>
<h3 className="text-center">
{user.username || user?.name || user?.email || "Anon"}
{user.username || user?.email || "Anon"}
</h3>
<div className="flex flex-col gap-2 justify-center w-full overflow-hidden">
{
@ -98,13 +98,17 @@ const UserProfileCard = ({ user }) => {
</div>
<div className='w-full flex flex-row justify-between'>
<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]">
<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>
) : (
<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
</h4>
<MoreInfo
@ -115,9 +119,23 @@ const UserProfileCard = ({ user }) => {
/>
</div>
)}
{user?.nip05 ? (
{user?.platformNip05 ? (
<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>
) : (
<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
</h4>
<MoreInfo
tooltip="PlebDevs Custom NIP-05"
modalTitle="PlebDevs Custom NIP-05"
modalBody="This is a placeholder for your PlebDevs issued NIP-05 (claimable through subscription)"
tooltip="NIP-05 Info"
modalTitle="What is NIP-05?"
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"
/>
</div>
@ -169,7 +187,7 @@ const UserProfileCard = ({ user }) => {
/>
</div>
<h3 className="self-start">
{user.username || user?.name || user?.email || "Anon"}
{user.username || user?.email || "Anon"}
</h3>
{
user?.pubkey && (
@ -193,10 +211,14 @@ const UserProfileCard = ({ user }) => {
)}
</div>
</div>
<div className="flex flex-col justify-between gap-4 my-2">
{user?.lightningAddress ? (
<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")} />
<div className="flex flex-col justify-between gap-2">
{user?.platformLightningAddress ? (
<h4 className="bg-gray-900 rounded-lg p-2 max-lap:w-fit min-w-[240px]">
<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>
) : (
<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>
)}
{user?.nip05 ? (
{user?.platformNip05 ? (
<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>
) : (
<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 [name, setName] = useState('');
const [description, setDescription] = useState('');
const [maxSendable, setMaxSendable] = useState(10000000);
const [minSendable, setMinSendable] = useState(1);
const [maxSendable, setMaxSendable] = useState(10000000000);
const [minSendable, setMinSendable] = useState(1000);
const [invoiceMacaroon, setInvoiceMacaroon] = useState('');
const [lndCert, setLndCert] = useState('');
const [lndHost, setLndHost] = useState('');
@ -26,18 +26,18 @@ const LightningAddressForm = ({ visible, onHide }) => {
const { showToast } = useToast();
useEffect(() => {
if (session && session?.user && !session?.user?.lightningAddress) {
setName(session.user.name || '');
} else if (session && session?.user && session?.user?.lightningAddress) {
setExistingLightningAddress(session.user.lightningAddress);
setName(session.user.lightningAddress.name || '');
setDescription(session.user.lightningAddress.description || '');
setMaxSendable(session.user.lightningAddress.maxSendable || 10000000);
setMinSendable(session.user.lightningAddress.minSendable || 1);
setInvoiceMacaroon(session.user.lightningAddress.invoiceMacaroon || '');
setLndCert(session.user.lightningAddress.lndCert || '');
setLndHost(session.user.lightningAddress.lndHost || '');
setLndPort(session.user.lightningAddress.lndPort || '8080');
if (session && session?.user && !session?.user?.platformLightningAddress) {
setName(session.user.username || '');
} else if (session && session?.user && session?.user?.platformLightningAddress) {
setExistingLightningAddress(session.user.platformLightningAddress);
setName(session.user.platformLightningAddress.name || '');
setDescription(session.user.platformLightningAddress.description || '');
setMaxSendable(Number(session.user.platformLightningAddress.maxSendable) || 10000000000);
setMinSendable(Number(session.user.platformLightningAddress.minSendable) || 1000);
setInvoiceMacaroon(session.user.platformLightningAddress.invoiceMacaroon || '');
setLndCert(session.user.platformLightningAddress.lndCert || '');
setLndHost(session.user.platformLightningAddress.lndHost || '');
setLndPort(session.user.platformLightningAddress.lndPort || '8080');
}
}, [session]);
@ -46,10 +46,21 @@ const LightningAddressForm = ({ visible, onHide }) => {
try {
let response;
const lowercaseName = name.toLowerCase();
const data = {
name: lowercaseName,
description,
maxSendable: BigInt(maxSendable).toString(),
minSendable: BigInt(minSendable).toString(),
invoiceMacaroon,
lndCert,
lndHost,
lndPort
};
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 {
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) {
showToast('success', 'Lightning Address Claimed', 'Your Lightning Address has been claimed');
@ -101,10 +112,9 @@ const LightningAddressForm = ({ visible, onHide }) => {
<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' />
<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.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)' />
<InputNumber placeholder="Max Sendable" value={maxSendable} onChange={(e) => setMaxSendable(e.value)} min={1000} tooltip='Maximum amount in millisats (1000 millisats = 1 sat)' />
<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>
<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>

View File

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

View File

@ -7,7 +7,6 @@ import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast';
import { Card } from 'primereact/card';
import { Badge } from 'primereact/badge';
import GenericButton from '@/components/buttons/GenericButton';
import { Menu } from "primereact/menu";
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",
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",
command: () => {
setNip05Visible(true);
@ -152,14 +151,14 @@ const SubscribeModal = ({ user }) => {
{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="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>
</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="mt-3">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Recurring subscription will AUTO renew on {subscribedUntil.toLocaleDateString()}</p>
</div>
)}
@ -168,7 +167,7 @@ const SubscribeModal = ({ user }) => {
<Message className="w-fit" severity="info" text="You currently have no active subscription" />
<GenericButton
label="Subscribe"
className="w-auto mt-4 text-[#f8f8ff]"
className="w-auto mt-3 text-[#f8f8ff]"
onClick={() => setVisible(true)}
/>
</div>
@ -232,7 +231,7 @@ const SubscribeModal = ({ user }) => {
visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username}
userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email}
/>
<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 { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast';
import axios from 'axios';
import { Card } from 'primereact/card';
@ -21,9 +20,7 @@ import RenewSubscription from '@/components/profile/subscription/RenewSubscripti
const UserSubscription = () => {
const { data: session, update } = useSession();
const { showToast } = useToast();
const router = useRouter();
const windowWidth = useWindowWidth();
const menu = useRef(null);
const [user, setUser] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [subscribed, setSubscribed] = useState(false);
@ -178,8 +175,8 @@ const UserSubscription = () => {
<div className="flex flex-col">
<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="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="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="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?.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>
)}
@ -225,7 +222,7 @@ const UserSubscription = () => {
visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username}
userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email}
/>
<CancelSubscription

View File

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

View File

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

View File

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

View File

@ -33,14 +33,19 @@ const syncNostrProfile = async (pubkey) => {
let dbUser = await getUserByPubkey(pubkey);
if (dbUser) {
// Update existing user if kind0 fields differ
if (fields.avatar !== dbUser.avatar || fields.username !== dbUser.username) {
// Update existing user if any of the kind0 fields differ
if (fields.avatar !== dbUser.avatar ||
fields.username !== dbUser.username ||
fields.lud16 !== dbUser.lud16 ||
fields.nip05 !== dbUser.nip05) {
const updates = {
...(fields.avatar !== dbUser.avatar && { avatar: fields.avatar }),
...(fields.username !== dbUser.username && {
username: fields.username,
name: fields.username
})
username: fields.username
}),
...(fields.lud16 !== dbUser.lud16 && { lud16: fields.lud16 }),
...(fields.nip05 !== dbUser.nip05 && { nip05: fields.nip05 })
};
await updateUser(dbUser.id, updates);
dbUser = await getUserByPubkey(pubkey);
@ -48,11 +53,14 @@ const syncNostrProfile = async (pubkey) => {
} else {
// Create new user
const username = fields.username || pubkey.slice(0, 8);
const lud16 = fields.lud16 || null;
const nip05 = fields.nip05 || null;
const payload = {
pubkey,
username,
avatar: fields.avatar,
name: username
lud16,
nip05
};
dbUser = await createUser(payload);
@ -126,8 +134,7 @@ export const authOptions = {
if (!user) {
user = await createUser({
...keys,
username: `anon-${keys.pubkey.slice(0, 8)}`,
name: `anon-${keys.pubkey.slice(0, 8)}`
username: `anon-${keys.pubkey.slice(0, 8)}`
});
}
return { ...user, privkey: keys.privkey };
@ -157,7 +164,7 @@ export const authOptions = {
id: profile.id.toString(),
pubkey: keys.pubkey,
privkey: keys.privkey,
name: profile.login,
username: profile.login,
email: profile.email,
avatar: profile.avatar_url
};
@ -194,8 +201,8 @@ export const authOptions = {
purchased: true,
userCourses: true,
userLessons: true,
nip05: true,
lightningAddress: true,
platformNip05: true,
platformLightningAddress: true,
userBadges: true
}
});
@ -232,7 +239,6 @@ export const authOptions = {
username: user.email.split('@')[0],
email: user.email,
avatar: user.image,
name: user.email.split('@')[0],
}
// Update the user with the new keypair
@ -256,7 +262,15 @@ export const authOptions = {
if (userData) {
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
const githubAccount = await prisma.account.findFirst({
where: {
@ -273,13 +287,14 @@ export const authOptions = {
role: fullUser.role,
username: fullUser.username,
avatar: fullUser.avatar,
name: fullUser.name,
email: fullUser.email,
userCourses: fullUser.userCourses,
userLessons: fullUser.userLessons,
purchased: fullUser.purchased,
nip05: fullUser.nip05,
lightningAddress: fullUser.lightningAddress,
lud16: fullUser.lud16,
platformNip05: fullUser.platformNip05,
platformLightningAddress: fullUser.platformLightningAddress,
githubUsername: token.githubUsername,
createdAt: fullUser.createdAt,
userBadges: fullUser.userBadges
@ -300,15 +315,22 @@ export const authOptions = {
return 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 (account?.provider === "github" && user?.id && user?.pubkey && user?.privkey) {
try {
// First update the user's profile with GitHub info
const updatedUser = await updateUser(user.id, {
name: profile?.login || profile?.name,
username: profile?.login || profile?.name,
avatar: profile?.avatar_url,
image: profile?.avatar_url,
});
// Get the updated user
@ -341,10 +363,8 @@ export const authOptions = {
if (!existingGithubAccount) {
// Update user profile with GitHub info
const updatedUser = await updateUser(user.id, {
name: profile?.login || profile?.name,
username: profile?.login || profile?.name,
avatar: profile?.avatar_url,
image: profile?.avatar_url,
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');
}
// Convert amount from millisatoshis to satoshis
if (amount < (foundAddress.minSendable)) {
// Check amount against BigInt min/max values
if (amount < foundAddress.minSendable) {
res.status(400).json({ error: 'Amount too low' });
return;
} else if (amount > (foundAddress.maxSendable || Number.MAX_SAFE_INTEGER)) {
} else if (amount > foundAddress.maxSendable) {
res.status(400).json({ error: 'Amount too high' });
return;
} else {
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: {
'Authorization': PLEBDEVS_API_KEY
}

View File

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

View File

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

View File

@ -327,7 +327,7 @@ export default function Draft() {
<p className='text-lg'>
Created by{' '}
<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>
</p>
)}

View File

@ -101,12 +101,12 @@ const Subscribe = () => {
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",
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",
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 (
<div className="p-4">
{windowWidth < 768 && (
@ -242,8 +225,8 @@ const Subscribe = () => {
<Card title="Subscription Benefits" className="mb-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="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="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="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?.platformLightningAddress ? "Update Lightning Address" : "Claim PlebDevs Lightning Address"} icon={<i style={{ color: "orange" }} className="pi pi-bolt mr-2"></i>} onClick={() => setLightningAddressVisible(true)} />
</div>
</Card>
<Card title="Manage Subscription" className="mb-4">
@ -296,7 +279,7 @@ const Subscribe = () => {
visible={calendlyVisible}
onHide={() => setCalendlyVisible(false)}
userId={session?.user?.id}
userName={session?.user?.name || user?.kind0?.username}
userName={session?.user?.username || user?.kind0?.username}
userEmail={session?.user?.email}
/>
<CancelSubscription

View File

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