Nostr login flow

This commit is contained in:
austinkelsay 2024-02-11 00:00:27 -06:00
parent 2de8b23d07
commit 1f9d76d783
15 changed files with 702 additions and 33 deletions

View File

@ -21,6 +21,8 @@ services:
- .env
ports:
- "3000:3000"
volumes:
- .:/app
links:
- db

292
package-lock.json generated
View File

@ -8,9 +8,12 @@
"name": "plebdevs-new",
"version": "0.1.0",
"dependencies": {
"@prisma/client": "^5.9.1",
"@reduxjs/toolkit": "^2.1.0",
"axios": "^1.6.7",
"next": "14.0.4",
"next-auth": "^4.24.5",
"nostr-tools": "^2.1.5",
"primeicons": "^6.0.1",
"primereact": "^10.2.1",
"react": "^18",
@ -23,6 +26,7 @@
"eslint": "^8",
"eslint-config-next": "14.0.4",
"postcss": "^8",
"prisma": "^5.9.1",
"tailwindcss": "^3.3.0"
}
},
@ -388,6 +392,47 @@
"node": ">= 10"
}
},
"node_modules/@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -441,6 +486,68 @@
"node": ">=14"
}
},
"node_modules/@prisma/client": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.9.1.tgz",
"integrity": "sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ==",
"hasInstallScript": true,
"engines": {
"node": ">=16.13"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.9.1.tgz",
"integrity": "sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==",
"devOptional": true
},
"node_modules/@prisma/engines": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.9.1.tgz",
"integrity": "sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.9.1",
"@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
"@prisma/fetch-engine": "5.9.1",
"@prisma/get-platform": "5.9.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz",
"integrity": "sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ==",
"devOptional": true
},
"node_modules/@prisma/fetch-engine": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz",
"integrity": "sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.9.1",
"@prisma/engines-version": "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64",
"@prisma/get-platform": "5.9.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.9.1.tgz",
"integrity": "sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.9.1"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz",
@ -470,6 +577,53 @@
"integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
"dev": true
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@ -871,6 +1025,11 @@
"has-symbols": "^1.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
@ -929,6 +1088,16 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@ -1147,6 +1316,17 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -1261,6 +1441,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -2007,6 +2195,25 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -2032,6 +2239,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2985,6 +3205,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3179,6 +3418,36 @@
"node": ">=0.10.0"
}
},
"node_modules/nostr-tools": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz",
"integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==",
"dependencies": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"optional": true
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
@ -3717,6 +3986,22 @@
}
}
},
"node_modules/prisma": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.9.1.tgz",
"integrity": "sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.9.1"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=16.13"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -3727,6 +4012,11 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -4648,7 +4938,7 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",

View File

@ -9,9 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"@prisma/client": "^5.9.1",
"@reduxjs/toolkit": "^2.1.0",
"axios": "^1.6.7",
"next": "14.0.4",
"next-auth": "^4.24.5",
"nostr-tools": "^2.1.5",
"primeicons": "^6.0.1",
"primereact": "^10.2.1",
"react": "^18",
@ -24,6 +27,7 @@
"eslint": "^8",
"eslint-config-next": "14.0.4",
"postcss": "^8",
"prisma": "^5.9.1",
"tailwindcss": "^3.3.0"
}
}

View File

@ -0,0 +1,87 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"pubkey" TEXT NOT NULL,
"username" TEXT,
"roleId" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Role" (
"id" SERIAL NOT NULL,
"subscribed" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Purchase" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"courseId" INTEGER,
"resourceId" INTEGER,
"amountPaid" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Course" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"image" TEXT NOT NULL,
"isFree" BOOLEAN NOT NULL DEFAULT false,
"noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Course_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Resource" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"image" TEXT,
"courseId" INTEGER,
"noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Resource_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_pubkey_key" ON "User"("pubkey");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Course_noteId_key" ON "Course"("noteId");
-- CreateIndex
CREATE UNIQUE INDEX "Resource_noteId_key" ON "Resource"("noteId");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "Resource"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Resource" ADD CONSTRAINT "Resource_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -3,9 +3,13 @@ datasource db {
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
npub String @unique
pubkey String @unique
username String? @unique
purchased Purchase[]
role Role? @relation(fields: [roleId], references: [id])

View File

@ -1,19 +1,27 @@
import React from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { Button } from 'primereact/button';
import { Menubar } from 'primereact/menubar';
import { useSelector } from 'react-redux';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';
import styles from './navbar.module.css';
const Navbar = () => {
const router = useRouter();
const user = useSelector((state) => state.user);
const end = (
(user && user?.username || user.pubkey) ?
<h1>{user.username || user.pubkey}</h1>
:
<Button
label={"Login"}
icon="pi pi-user"
className="text-[#f8f8ff]"
rounded
onClick={() => router.push('/login')}
/>
);

View File

@ -29,6 +29,21 @@ export const getUserById = async (id) => {
});
};
export const getUserByPubkey = async (pubkey) => {
return await prisma.user.findUnique({
where: { pubkey },
include: {
role: true, // Include related role
purchased: {
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
},
},
},
});
}
export const createUser = async (data) => {
return await prisma.user.create({
data,

106
src/hooks/useNostr.js Normal file
View File

@ -0,0 +1,106 @@
import { useState, useEffect, useRef } from "react";
import { SimplePool, relayInit, nip19 } from "nostr-tools";
import { useDispatch } from "react-redux";
import { initialRelays } from "@/redux/reducers/userReducer";
export const useNostr = () => {
const [relays, setRelays] = useState(initialRelays);
const [relayStatuses, setRelayStatuses] = useState({});
const dispatch = useDispatch();
const pool = useRef(new SimplePool({ seenOnEnabled: true }));
const subscriptions = useRef([]);
const getRelayStatuses = () => {
if (pool.current && pool.current._conn) {
const statuses = {};
for (const url in pool.current._conn) {
const relay = pool.current._conn[url];
statuses[url] = relay.status; // Assuming 'status' is an accessible field in Relay object
}
setRelayStatuses(statuses);
}
};
const updateRelays = async (newRelays) => {
// Set new relays
setRelays(newRelays);
// Ensure the relays are connected before using them
await Promise.all(newRelays.map(relay => pool.current.ensureRelay(relay)));
};
const fetchKind0 = async (criteria, params) => {
return new Promise((resolve, reject) => {
const events = [];
const timeoutDuration = 1000;
const sub = pool.current.subscribeMany(relays, criteria, {
...params,
onevent: (event) => {
events.push(event);
},
onerror: (error) => {
reject(error);
}
});
// Set a timeout to sort and resolve with the most recent event
setTimeout(() => {
if (events.length === 0) {
resolve(null); // or reject based on your needs
} else {
events.sort((a, b) => b.created_at - a.created_at); // Sort in descending order
const mostRecentEventContent = JSON.parse(events[0].content);
resolve(mostRecentEventContent);
}
}, timeoutDuration);
});
};
const fetchSingleEvent = async (id) => {
return new Promise((resolve, reject) => {
const sub = pool.current.subscribeMany(relays, [{ ids: [id] }]);
sub.on("event", (event) => {
resolve(event);
});
sub.on("error", (error) => {
reject(error);
});
});
};
const publishEvent = async (event) => {
try {
const publishPromises = pool.current.publish(relays, event);
await Promise.all(publishPromises);
} catch (error) {
console.error("Error publishing event:", error);
}
};
useEffect(() => {
getRelayStatuses(); // Get initial statuses on mount
// Copy current subscriptions to a local variable inside the effect
const currentSubscriptions = subscriptions.current;
return () => {
// Use the local variable in the cleanup function
currentSubscriptions.forEach((sub) => sub.unsub());
};
}, []);
return {
updateRelays,
fetchSingleEvent,
publishEvent,
fetchKind0,
getRelayStatuses,
};
};

21
src/hooks/useToast.js Normal file
View File

@ -0,0 +1,21 @@
import React, { createContext, useContext, useRef } from 'react';
import { Toast } from 'primereact/toast';
const ToastContext = createContext();
export const useToast = () => useContext(ToastContext);
export const ToastProvider = ({ children }) => {
const toast = useRef(null);
const showToast = (severity, summary, detail) => {
toast.current.show({ severity, summary, detail });
};
return (
<ToastContext.Provider value={{ showToast }}>
<Toast ref={toast} />
{children}
</ToastContext.Provider>
);
};

View File

@ -2,6 +2,7 @@ import { PrimeReactProvider } from 'primereact/api';
import { Provider } from "react-redux";
import { store } from "@/redux/store";
import Navbar from '@/components/navbar/Navbar';
import { ToastProvider } from '@/hooks/useToast';
import '@/styles/globals.css'
import 'primereact/resources/themes/lara-dark-indigo/theme.css';
@ -11,8 +12,10 @@ export default function MyApp({
return (
<Provider store={store}>
<PrimeReactProvider>
<Navbar />
<Component {...pageProps} />
<ToastProvider>
<Navbar />
<Component {...pageProps} />
</ToastProvider>
</PrimeReactProvider>
</Provider>
);

View File

@ -1,36 +1,59 @@
import { getUserById, updateUser, deleteUser } from "@/db/models/userModels";
import { getUserById, getUserByPubkey, updateUser, deleteUser } from "@/db/models/userModels";
export default async function handler(req, res) {
const { slug } = req.query;
if (req.method === 'GET') {
try {
const user = await getUserById(parseInt(slug));
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ error: 'User not found' });
// Determine if slug is a pubkey or an ID
const isPubkey = /^[0-9a-fA-F]{64}$/.test(slug);
try {
let user;
if (isPubkey) {
console.log('is pub', slug);
// If slug is a pubkey
user = await getUserByPubkey(slug);
} else {
// Assume slug is an ID
const id = parseInt(slug);
if (isNaN(id)) {
return res.status(400).json({ error: "Invalid identifier" });
}
} catch (error) {
res.status(500).json({ error: error.message });
user = await getUserById(id);
}
} else if (req.method === 'PUT') {
try {
const user = await updateUser(parseInt(slug), req.body);
res.status(200).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
if (!user) {
return res.status(204).end();
}
} else if (req.method === 'DELETE') {
try {
await deleteUser(parseInt(slug));
res.status(204).end();
} catch (error) {
res.status(500).json({ error: error.message });
switch (req.method) {
case 'GET':
return res.status(200).json(user);
case 'PUT':
if (!isPubkey) {
// Update operation should be done with an ID, not a pubkey
const updatedUser = await updateUser(parseInt(slug), req.body);
return res.status(200).json(updatedUser);
} else {
// Handle attempt to update user with pubkey
return res.status(400).json({ error: "Cannot update user with pubkey. Use ID instead." });
}
case 'DELETE':
if (!isPubkey) {
// Delete operation should be done with an ID, not a pubkey
await deleteUser(parseInt(slug));
return res.status(204).end();
} else {
// Handle attempt to delete user with pubkey
return res.status(400).json({ error: "Cannot delete user with pubkey. Use ID instead." });
}
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
} else {
// Handle any other HTTP method
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
} catch (error) {
return res.status(500).json({ error: error.message });
}
}

83
src/pages/login.js Normal file
View File

@ -0,0 +1,83 @@
import React from "react";
import axios from "axios";
import { useDispatch } from "react-redux";
import { useRouter } from "next/router";
import { Button } from 'primereact/button';
import { useToast } from "@/hooks/useToast";
import { useNostr } from "@/hooks/useNostr";
import { findKind0Username } from "@/utils/nostr";
import { setPubkey, setUsername } from "@/redux/reducers/userReducer";
const Login = () => {
const dispatch = useDispatch();
const router = useRouter();
const { showToast } = useToast();
const nostrLogin = async () => {
try {
if (!window || !window.nostr) {
throw new Error('Nostr is not available');
}
const publicKey = await window.nostr.getPublicKey();
if (!publicKey) {
throw new Error('Failed to obtain public key');
}
try {
const response = await axios.get(`/api/users/${publicKey}`);
if (response.status === 200 && response.data) {
dispatch(setPubkey(publicKey));
if (response.data.username) {
dispatch(setUsername(response.data.username));
}
router.push('/');
return;
}
} catch (error) {
if (error.response?.status !== 204) {
throw error; // Rethrow error if it's not the expected 204 status
}
// Handle user creation if status is 204 (No Content)
const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {});
const username = kind0 ? await findKind0Username(kind0) : undefined;
const payload = { pubkey: publicKey, ...(username && { username }) };
const createUserResponse = await axios.post(`/api/users`, payload);
if (createUserResponse.status === 201) {
dispatch(setPubkey(publicKey));
if (username) {
dispatch(setUsername(username));
}
router.push('/');
} else {
showToast('error', 'Error', 'User not created');
}
}
} catch (error) {
showToast('error', 'Error', error.message || 'An unexpected error occurred');
}
};
return (
<div className="w-fit mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Login</h1>
<Button
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
onClick={nostrLogin}
/>
<Button
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4"
rounded
/>
</div>
)
}
export default Login;

View File

@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
const initialRelays = [
export const initialRelays = [
"wss://nos.lol/",
"wss://relay.damus.io/",
"wss://relay.snort.social/",
@ -14,6 +14,7 @@ export const userSlice = createSlice({
name: "user",
initialState: {
pubkey: '',
username: '',
relays: initialRelays,
},
reducers: {
@ -22,10 +23,13 @@ export const userSlice = createSlice({
},
setPubkey: (state, action) => {
state.pubkey = action.payload;
}
},
setUsername: (state, action) => {
state.username = action.payload;
},
},
});
export const { setRelays, setPubkey } = userSlice.actions;
export const { setRelays, setPubkey, setUsername } = userSlice.actions;
export default userSlice.reducer;

16
src/utils/nostr.js Normal file
View File

@ -0,0 +1,16 @@
export const findKind0Username = async (kind0) => {
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
const findTruthyPropertyValue = (object, properties) => {
for (const property of properties) {
if (object[property]) {
return object[property];
}
}
return null;
};
const username = findTruthyPropertyValue(kind0, usernameProperties);
return username;
}