feat: base of linking api

This commit is contained in:
MTG2000
2022-08-08 17:11:59 +03:00
parent f833644da6
commit 940c4ba66c
13 changed files with 325 additions and 10 deletions

View File

@@ -58,12 +58,15 @@ function removeExpiredHashes() {
async function generateAuthUrl() {
async function generateAuthUrl(options) {
const hostname = CONSTS.LNURL_AUTH_HOST ?? 'https://auth.bolt.fun/.netlify/functions/login';
const secret = await generateK1();
const hash = createHash(secret);
await addHash(hash)
const url = `${hostname}?tag=login&k1=${secret}`
let url = `${hostname}?tag=login&k1=${secret}`
if (options.user_token) {
url = url + `&action=link&user_token=${options.user_token}`
}
return {
url,
encoded: lnurl.encode(url).toUpperCase(),

View File

@@ -3,9 +3,11 @@ const { prisma } = require('../../prisma')
const getUserByPubKey = (pubKey) => {
if (!pubKey) return null;
return prisma.user.findFirst({
where: { pubKey }
})
return prisma.userKey.findUnique({
where: {
key: pubKey
},
}).user()
}

View File

@@ -5,14 +5,35 @@ const { createExpressApp } = require('../../modules');
const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const extractKeyFromCookie = require('../../utils/extractKeyFromCookie');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const getLoginUrl = async (req, res) => {
const { action } = req.query;
try {
const data = await LnurlAuthService.generateAuthUrl();
let user_token = null;
if (action === 'link') {
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey);
if (!user)
return res.status(400).json({ status: 'ERROR', reason: 'Only authenticated user can request a linking URL' });
user_token = await new jose.SignJWT({ user_id: user.id })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('5min')
.sign(Buffer.from(JWT_SECRET, 'utf-8'))
}
const data = await LnurlAuthService.generateAuthUrl({ user_token });
const session_token = await new jose.SignJWT({ hash: data.secretHash })
.setProtectedHeader({ alg: 'HS256' })

View File

@@ -8,11 +8,13 @@ const express = require('express');
const jose = require('jose');
const { JWT_SECRET } = require('../../utils/consts');
const { generatePrivateKey, getPublicKey } = require('../../utils/nostr-tools');
const { getUserByPubKey } = require('../../auth/utils/helperFuncs');
const loginHandler = async (req, res) => {
const { tag, k1, sig, key } = req.query;
const { tag, k1, sig, key, action, user_token } = req.query;
if (tag !== 'login')
return res.status(400).json({ status: 'ERROR', reason: 'Invalid Tag Provided' })
@@ -24,24 +26,63 @@ const loginHandler = async (req, res) => {
}
if (action === 'link' && user_token) {
try {
const { payload } = await jose.jwtVerify(user_token, Buffer.from(JWT_SECRET), {
algorithms: ['HS256'],
})
const user_id = payload.user_id;
const existingKeys = await prisma.userKey.findMany({ where: { user_id }, select: { key: true } });
if (existingKeys.length >= 3)
return res.status(400).json({ status: 'ERROR', reason: "Can only link up to 3 wallets" })
if (existingKeys.includes(key))
return res.status(400).json({ status: 'ERROR', reason: "Wallet already linked" })
await prisma.userKey.create({
data: {
key,
user_id,
}
});
return res
.status(200)
.json({ status: "OK" })
} catch (error) {
return res.status(400).json({ status: 'ERROR', reason: 'Invalid User Token' })
}
}
try {
//Create user if not already existing
const user = await prisma.user.findFirst({ where: { pubKey: key } })
const user = await getUserByPubKey(key)
if (user === null) {
const nostr_prv_key = generatePrivateKey();
const nostr_pub_key = getPublicKey(nostr_prv_key);
await prisma.user.create({
const createdUser = await prisma.user.create({
data: {
pubKey: key,
name: key,
avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
nostr_prv_key,
nostr_pub_key,
}
},
})
await prisma.userKey.create({
data: {
key,
user_id: createdUser.id,
}
});
}
// calc the hash of k1

View File

@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "UserKey" (
"key" TEXT NOT NULL,
"user_id" INTEGER,
CONSTRAINT "UserKey_pkey" PRIMARY KEY ("key")
);
-- AddForeignKey
ALTER TABLE "UserKey" ADD CONSTRAINT "UserKey_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -64,6 +64,14 @@ model User {
questions Question[]
posts_comments PostComment[]
donations Donation[]
userKeys UserKey[]
}
model UserKey {
key String @id
user User? @relation(fields: [user_id], references: [id])
user_id Int?
}
// -----------------

View File

@@ -58,6 +58,8 @@ async function main() {
// await createHackathons();
await fillUserKeysTable()
}
async function createCategories() {
@@ -150,6 +152,22 @@ async function createHackathons() {
}
}
async function fillUserKeysTable() {
console.log('Filling Users Keys Table');
const allUsers = await prisma.user.findMany({
select: {
id: true,
pubKey: true,
}
})
await prisma.userKey.createMany({
data: allUsers.filter(u => !!u.pubKey).map(u => ({
key: u.pubKey,
user_id: u.id
}))
})
}

View File

@@ -0,0 +1,19 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import AccountCard from './AccountCard';
export default {
title: 'Profiles/Profile Page/Account Card',
component: AccountCard,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof AccountCard>;
const Template: ComponentStory<typeof AccountCard> = (args) => <AccountCard {...args} ></AccountCard>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,39 @@
import Button from 'src/Components/Button/Button';
import { useAppDispatch } from 'src/utils/hooks';
import { openModal } from 'src/redux/features/modals.slice';
interface Props {
}
export default function AccountCard({ }: Props) {
const dispatch = useAppDispatch()
const connectNewWallet = () => {
dispatch(openModal({ Modal: "LinkingAccountModal" }))
}
return (
<div className="rounded-16 bg-white border-2 border-gray-200 p-24">
<p className="text-body2 font-bold">Account Settings</p>
<div className='mt-24 flex flex-col gap-16'>
<p className="text-body3 font-bold">Linked Wallets</p>
<p className="text-body4 text-gray-600">
These are the wallets that you can login to this account from.
<br />
You can add a new wallet from the button below.
</p>
<Button color='primary' className='' onClick={connectNewWallet}>
Connect new wallet
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,143 @@
import { motion } from 'framer-motion'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { useEffect, useState } from "react"
import { Grid } from "react-loader-spinner";
import { CONSTS } from "src/utils";
import { QRCodeSVG } from 'qrcode.react';
import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url', {
credentials: 'include'
})
const data = await res.json()
return data;
}
const useLnurlQuery = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<any>(null);
const [data, setData] = useState<{ lnurl: string, session_token: string }>({ lnurl: '', session_token: '' })
useEffect(() => {
let timeOut: NodeJS.Timeout;
const doFetch = async () => {
const res = await fetchLnurlAuth();
if (!res?.encoded)
setError(true)
else {
setLoading(false);
setData({
lnurl: res.encoded,
session_token: res.session_token
});
timeOut = setTimeout(doFetch, 1000 * 60 * 2)
}
}
doFetch()
return () => clearTimeout(timeOut)
}, [])
return {
loadingLnurl: loading,
error,
data
}
}
export default function LinkingAccountModal({ onClose, direction, ...props }: ModalCard) {
const [copied, setCopied] = useState(false);
const { loadingLnurl, data: { lnurl }, error } = useLnurlQuery();
const clipboard = useCopyToClipboard()
useEffect(() => {
setCopied(false);
}, [lnurl])
const copyToClipboard = () => {
setCopied(true);
clipboard(lnurl);
}
let content = <></>
if (error)
content = <div className="flex flex-col gap-24 items-center">
<p className="text-body3 text-red-500 font-bold">Something wrong happened...</p>
<a href='/login' className="text body4 text-gray-500 hover:underline">Refresh the page</a>
</div>
else if (loadingLnurl)
content = <div className="flex flex-col gap-24 items-center">
<Grid color="var(--primary)" width="150" />
<p className="text-body3 font-bold">Fetching Lnurl-Auth...</p>
</div>
else
content =
<>
<p className="text-body1 font-bolder text-center">
Link your account
</p>
<QRCodeSVG
width={160}
height={160}
value={lnurl}
/>
<p className="text-gray-600 text-body4 text-center">
Scan this code or copy + paste it to your other lightning wallet to be able to login later with it to this account.
<br />
When done, click the button below to close this modal.
</p>
<div className="flex flex-col w-full gap-16">
{/* <a href={lnurl}
className='grow block text-body4 text-center text-white font-bolder bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
>Click to connect <IoRocketOutline /></a> */}
<Button
color='gray'
className='grow'
onClick={copyToClipboard}
fullWidth
>{copied ? "Copied" : "Copy"} <FiCopy /></Button>
<Button
color='primary'
onClick={onClose}
fullWidth
className='mt-16'
>
Done?
</Button>
</div>
</>
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card w-full max-w-[326px] bg-white border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-16 items-center"
>
{content}
</motion.div>
)
}

View File

@@ -0,0 +1,3 @@
import { lazyModal } from 'src/utils/helperFunctions';
export const { LazyComponent: LinkingAccountModal } = lazyModal(() => import('./LinkingAccountModal'))

View File

@@ -8,6 +8,7 @@ import CommentsSettingsCard from "../ProfilePage/CommentsSettingsCard/CommentsSe
import UpdateMyProfileCard from "./UpdateMyProfileCard/UpdateMyProfileCard";
import { Helmet } from 'react-helmet'
import { MEDIA_QUERIES } from "src/utils/theme";
import AccountCard from "./AccountCard/AccountCard";
const links = [
@@ -15,6 +16,10 @@ const links = [
text: "👾 My Profile",
path: 'my-profile',
},
{
text: "🙍‍♂️ Account",
path: 'account',
},
{
text: "⚙️ Preferences",
path: 'preferences',
@@ -92,6 +97,7 @@ export default function EditProfilePage() {
<Routes>
<Route index element={<Navigate to='my-profile' />} />
<Route path='my-profile' element={<UpdateMyProfileCard data={profileQuery.data.profile} />} />
<Route path='account' element={<AccountCard />} />
<Route path='preferences' element={<CommentsSettingsCard nostr_prv_key={profileQuery.data.profile.nostr_prv_key} nostr_pub_key={profileQuery.data.profile.nostr_pub_key} isOwner={true} />
} />
</Routes>

View File

@@ -7,6 +7,7 @@ import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoMo
import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject";
import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer";
import { ConfirmModal } from "src/Components/Modals/ConfirmModal";
import { LinkingAccountModal } from "src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal";
import { ComponentProps } from "react";
import { generateId } from "src/utils/helperFunctions";
@@ -32,6 +33,7 @@ export const ALL_MODALS = {
Claim_SubmittedCard,
Claim_FundWithdrawCard,
ConfirmModal,
LinkingAccountModal,
// Text Editor Modals
InsertImageModal,