mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-05 23:44:26 +01:00
feat: lnurl-auth login
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
|
||||
REACT_APP_API_END_POINT = https://makers.bolt.fun/.netlify/functions/graphql
|
||||
REACT_APP_GRAPHQL_END_POINT = https://makers.bolt.fun/.netlify/functions/graphql
|
||||
REACT_APP_AUTH_END_POINT = https://makers.bolt.fun/.netlify/functions/auth
|
||||
@@ -1 +1,2 @@
|
||||
REACT_APP_API_END_POINT = http://localhost:8888/dev/graphql
|
||||
REACT_APP_GRAPHQL_END_POINT = http://localhost:8888/dev/graphql
|
||||
REACT_APP_AUTH_END_POINT = http://localhost:8888/dev/auth
|
||||
@@ -1,2 +1,3 @@
|
||||
REACT_APP_FOR_GITHUB=true
|
||||
REACT_APP_API_END_POINT = https://makers.bolt.fun/.netlify/functions/graphql
|
||||
REACT_APP_GRAPHQL_END_POINT = https://makers.bolt.fun/.netlify/functions/graphql
|
||||
REACT_APP_AUTH_END_POINT = https://makers.bolt.fun/.netlify/functions/auth
|
||||
87
functions/auth/login.js
Normal file
87
functions/auth/login.js
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
const { prisma } = require('../prisma');
|
||||
const LnurlService = require('./services/lnurl.service')
|
||||
const cookie = require('cookie')
|
||||
const jose = require('jose');
|
||||
const { CONSTS } = require('../utils');
|
||||
|
||||
|
||||
async function generateAuthUrl() {
|
||||
const data = await LnurlService.generateAuthUrl();
|
||||
return {
|
||||
status: "OK",
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function login(tag, k1, sig, key) {
|
||||
if (tag !== 'login') {
|
||||
return { status: 'ERROR', reason: 'Not a login request' }
|
||||
}
|
||||
|
||||
const result = LnurlService.verifySig(sig, k1, key)
|
||||
if (!result) {
|
||||
return { status: 'ERROR', reason: 'Invalid Signature' }
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({ where: { pubKey: key } })
|
||||
if (user === null) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
pubKey: key,
|
||||
name: key,
|
||||
avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Set cookies on the user's headers
|
||||
const hour = 3600000
|
||||
const maxAge = 30 * 24 * hour
|
||||
const jwtSecret = CONSTS.JWT_SECRET;
|
||||
|
||||
|
||||
const jwt = await new jose.SignJWT({ pubKey: key })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(maxAge)
|
||||
//TODO: Set audience, issuer
|
||||
.sign(Buffer.from(jwtSecret, 'utf-8'))
|
||||
|
||||
|
||||
|
||||
const authCookie = cookie.serialize('Authorization', `Bearer ${jwt}`, {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
maxAge: maxAge,
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'OK',
|
||||
'headers': {
|
||||
'Set-Cookie': authCookie,
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
const { tag, k1, sig, key } = event.queryStringParameters ?? {}
|
||||
|
||||
if (event.httpMethod !== "GET") {
|
||||
return { statusCode: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
if (!sig || !key) {
|
||||
return generateAuthUrl();
|
||||
}
|
||||
else {
|
||||
return login(tag, k1, sig, key)
|
||||
}
|
||||
};
|
||||
29
functions/auth/logout.js
Normal file
29
functions/auth/logout.js
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
const cookie = require('cookie')
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
// const hour = 3600000
|
||||
// const maxAge = 30 * 24 * hour;
|
||||
// const _cookie = cookie.serialize('Hello', `Bearer hello`, {
|
||||
// // secure: true,
|
||||
// // httpOnly: true,
|
||||
// path: '/',
|
||||
// maxAge
|
||||
// })
|
||||
const hour = 3600000
|
||||
const twoWeeks = 14 * 24 * hour
|
||||
const myCookie = cookie.serialize('Authorization', '', {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
maxAge: -1,
|
||||
})
|
||||
console.log(myCookie);
|
||||
return {
|
||||
status: 'OK',
|
||||
'headers': {
|
||||
'Set-Cookie': myCookie,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
}
|
||||
};
|
||||
97
functions/auth/services/lnurl.service.js
Normal file
97
functions/auth/services/lnurl.service.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const lnurl = require('lnurl')
|
||||
const crypto = require('crypto')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
|
||||
async function generateSecret() {
|
||||
let secret = null
|
||||
const maxAttempts = 5
|
||||
let attempt = 0
|
||||
while (secret === null && attempt < maxAttempts) {
|
||||
secret = crypto.randomBytes(32).toString('hex')
|
||||
const hash = createHash(secret)
|
||||
const isUsed = await isHashUsed(hash);
|
||||
if (isUsed) {
|
||||
secret = null
|
||||
}
|
||||
attempt++
|
||||
}
|
||||
if (!secret) {
|
||||
const message = 'Too many failed attempts to generate unique secret'
|
||||
throw new Error(message)
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
function isHashUsed(hash) {
|
||||
return prisma.generatedK1.findFirst({ where: { value: hash } })
|
||||
}
|
||||
|
||||
function addHash(hash) {
|
||||
prisma.generatedK1.create({
|
||||
data: {
|
||||
value: hash,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeHash(hash) {
|
||||
prisma.generatedK1.delete({
|
||||
where: {
|
||||
value: hash,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeExpiredHashes() {
|
||||
const now = new Date();
|
||||
const lastHourDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours() - 1, now.getMinutes());
|
||||
|
||||
prisma.generatedK1.deleteMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
lt: lastHourDate
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function generateAuthUrl() {
|
||||
const hostname = 'http://localhost:8888/dev/auth/login'
|
||||
const secret = await generateSecret()
|
||||
addHash(createHash(secret))
|
||||
const url = `${hostname}?tag=login&k1=${secret}`
|
||||
return {
|
||||
url,
|
||||
encoded: lnurl.encode(url).toUpperCase(),
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
function verifySig(sig, k1, key) {
|
||||
if (!lnurl.verifyAuthorizationSignature(sig, k1, key)) {
|
||||
const message = 'Signature verification failed'
|
||||
throw new Error(message)
|
||||
}
|
||||
const hash = createHash(k1)
|
||||
return { key, hash }
|
||||
}
|
||||
|
||||
function createHash(data) {
|
||||
if (!(typeof data === 'string' || Buffer.isBuffer(data))) {
|
||||
throw new Error(
|
||||
JSON.stringify({ status: 'ERROR', reason: 'Secret must be a string or a Buffer' })
|
||||
)
|
||||
}
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'hex')
|
||||
}
|
||||
return crypto.createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateAuthUrl: generateAuthUrl,
|
||||
verifySig: verifySig,
|
||||
removeHash: removeHash,
|
||||
removeExpiredHashes: removeExpiredHashes
|
||||
}
|
||||
13
functions/auth/utils/helperFuncs.js
Normal file
13
functions/auth/utils/helperFuncs.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
const getUserByPubKey = (pubKey) => {
|
||||
if (!pubKey) return null;
|
||||
return prisma.user.findFirst({
|
||||
where: { pubKey }
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUserByPubKey,
|
||||
}
|
||||
@@ -1,12 +1,27 @@
|
||||
const { ApolloServer } = require("apollo-server-lambda");
|
||||
const schema = require('./schema')
|
||||
const cookie = require('cookie')
|
||||
const jose = require('jose');
|
||||
const { CONSTS } = require('../utils');
|
||||
|
||||
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
|
||||
context: () => {
|
||||
context: async ({ event }) => {
|
||||
const cookies = cookie.parse(event.headers.Cookie ?? '');
|
||||
const authToken = cookies.Authorization;
|
||||
if (authToken) {
|
||||
const token = authToken.split(' ')[1];
|
||||
const { payload } = await jose.jwtVerify(token, Buffer.from(CONSTS.JWT_SECRET), {
|
||||
algorithms: ['HS256'],
|
||||
})
|
||||
return { userPubKey: payload.pubKey }
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -22,6 +37,7 @@ const apolloHandler = server.createHandler({
|
||||
|
||||
// https://github.com/vendia/serverless-express/issues/427#issuecomment-924580007
|
||||
const handler = (event, context, ...args) => {
|
||||
|
||||
return apolloHandler(
|
||||
{
|
||||
...event,
|
||||
|
||||
@@ -296,6 +296,7 @@ export interface NexusGenFieldTypes {
|
||||
getProject: NexusGenRootTypes['Project']; // Project!
|
||||
getTrendingPosts: NexusGenRootTypes['Post'][]; // [Post!]!
|
||||
hottestProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
me: NexusGenRootTypes['User'] | null; // User
|
||||
newProjects: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
popularTopics: NexusGenRootTypes['Topic'][]; // [Topic!]!
|
||||
projectsByCategory: NexusGenRootTypes['Project'][]; // [Project!]!
|
||||
@@ -475,6 +476,7 @@ export interface NexusGenFieldTypeNames {
|
||||
getProject: 'Project'
|
||||
getTrendingPosts: 'Post'
|
||||
hottestProjects: 'Project'
|
||||
me: 'User'
|
||||
newProjects: 'Project'
|
||||
popularTopics: 'Topic'
|
||||
projectsByCategory: 'Project'
|
||||
|
||||
@@ -145,6 +145,7 @@ type Query {
|
||||
getProject(id: Int!): Project!
|
||||
getTrendingPosts: [Post!]!
|
||||
hottestProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
me: User
|
||||
newProjects(skip: Int = 0, take: Int = 50): [Project!]!
|
||||
popularTopics: [Topic!]!
|
||||
projectsByCategory(category_id: Int!, skip: Int = 0, take: Int = 10): [Project!]!
|
||||
|
||||
@@ -4,7 +4,7 @@ const {
|
||||
extendType,
|
||||
nonNull,
|
||||
} = require('nexus');
|
||||
const { prisma } = require('../prisma')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
|
||||
const Category = objectType({
|
||||
|
||||
@@ -7,8 +7,8 @@ const {
|
||||
extendType,
|
||||
nonNull,
|
||||
} = require('nexus');
|
||||
const { BOLT_FUN_LIGHTNING_ADDRESS } = require('../helpers/consts');
|
||||
const { prisma } = require('../prisma');
|
||||
const { prisma } = require('../../prisma');
|
||||
const { CONSTS } = require('../../utils');
|
||||
const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers');
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const donateMutation = extendType({
|
||||
resolve: async (_, args) => {
|
||||
|
||||
const { amount_in_sat } = args;
|
||||
const lightning_address = BOLT_FUN_LIGHTNING_ADDRESS;
|
||||
const lightning_address = CONSTS.BOLT_FUN_LIGHTNING_ADDRESS;
|
||||
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
|
||||
const invoice = parsePaymentRequest({ request: pr });
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const {
|
||||
extendType,
|
||||
nonNull,
|
||||
} = require('nexus');
|
||||
const { prisma } = require('../prisma')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
arg,
|
||||
} = require('nexus');
|
||||
const { paginationArgs } = require('./helpers');
|
||||
const { prisma } = require('../prisma')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
|
||||
const POST_TYPE = enumType({
|
||||
|
||||
@@ -5,7 +5,7 @@ const {
|
||||
extendType,
|
||||
nonNull,
|
||||
} = require('nexus')
|
||||
const { prisma } = require('../prisma')
|
||||
const { prisma } = require('../../prisma')
|
||||
|
||||
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const { objectType } = require("nexus");
|
||||
const { objectType, extendType } = require("nexus");
|
||||
const { getUserByPubKey } = require("../../auth/utils/helperFuncs");
|
||||
|
||||
|
||||
|
||||
const User = objectType({
|
||||
name: 'User',
|
||||
@@ -10,7 +13,23 @@ const User = objectType({
|
||||
})
|
||||
|
||||
|
||||
const me = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.field('me', {
|
||||
type: "User",
|
||||
async resolve(parent, args, context) {
|
||||
const user = await getUserByPubKey(context.userPubKey)
|
||||
return user
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
// Types
|
||||
User
|
||||
User,
|
||||
|
||||
// Queries
|
||||
me,
|
||||
}
|
||||
@@ -10,8 +10,9 @@ const {
|
||||
const { parsePaymentRequest } = require('invoices');
|
||||
const { getPaymetRequestForItem, hexToUint8Array } = require('./helpers');
|
||||
const { createHash } = require('crypto');
|
||||
const { prisma } = require('../prisma');
|
||||
const { BOLT_FUN_LIGHTNING_ADDRESS } = require('../helpers/consts');
|
||||
const { prisma } = require('../../prisma');
|
||||
const { CONSTS } = require('../../utils');
|
||||
|
||||
|
||||
|
||||
// the types of items we can vote to
|
||||
@@ -131,7 +132,7 @@ const voteMutation = extendType({
|
||||
resolve: async (_, args) => {
|
||||
|
||||
const { item_id, item_type, amount_in_sat } = args;
|
||||
const lightning_address = (await getLightningAddress(item_id, item_type)) ?? BOLT_FUN_LIGHTNING_ADDRESS;
|
||||
const lightning_address = (await getLightningAddress(item_id, item_type)) ?? CONSTS.BOLT_FUN_LIGHTNING_ADDRESS;
|
||||
const pr = await getPaymetRequestForItem(lightning_address, args.amount_in_sat);
|
||||
const invoice = parsePaymentRequest({ request: pr });
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const BOLT_FUN_LIGHTNING_ADDRESS = 'johns@getalby.com'; // #TODO, replace it by bolt-fun lightning address if there exist one
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
|
||||
module.exports = {
|
||||
const CONSTS = {
|
||||
JWT_SECRET,
|
||||
BOLT_FUN_LIGHTNING_ADDRESS,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CONSTS;
|
||||
4
functions/utils/index.js
Normal file
4
functions/utils/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
module.exports = {
|
||||
CONSTS: require('./consts')
|
||||
}
|
||||
1099
package-lock.json
generated
1099
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,14 +24,18 @@
|
||||
"apollo-server-lambda": "^3.6.7",
|
||||
"axios": "^0.26.1",
|
||||
"chance": "^1.1.8",
|
||||
"cookie": "^0.5.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"framer-motion": "^6.3.0",
|
||||
"fslightbox-react": "^1.6.2-2",
|
||||
"graphql": "^16.3.0",
|
||||
"invoices": "^2.0.6",
|
||||
"jose": "^4.8.1",
|
||||
"linkify-html": "^3.0.5",
|
||||
"linkifyjs": "^3.0.5",
|
||||
"lnurl": "^0.24.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^4.0.14",
|
||||
@@ -61,6 +65,7 @@
|
||||
"react-tooltip": "^4.2.21",
|
||||
"react-topbar-progress-indicator": "^4.1.1",
|
||||
"remirror": "^1.0.77",
|
||||
"secp256k1": "^4.0.3",
|
||||
"typescript": "^4.6.3",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webln": "^0.3.0",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[pubKey]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "email" TEXT,
|
||||
ADD COLUMN "pubKey" TEXT,
|
||||
ADD COLUMN "website" TEXT,
|
||||
ALTER COLUMN "avatar" DROP NOT NULL,
|
||||
ALTER COLUMN "name" DROP NOT NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GeneratedK1" (
|
||||
"value" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GeneratedK1_pkey" PRIMARY KEY ("value")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_pubKey_key" ON "User"("pubKey");
|
||||
@@ -36,10 +36,14 @@ model Vote {
|
||||
// -----------------
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
id Int @id @default(autoincrement())
|
||||
pubKey String? @unique
|
||||
|
||||
name String? @unique
|
||||
email String?
|
||||
website String?
|
||||
lightning_address String?
|
||||
avatar String
|
||||
avatar String?
|
||||
|
||||
stories Story[]
|
||||
questions Question[]
|
||||
@@ -199,3 +203,11 @@ model Donation {
|
||||
donor User? @relation(fields: [donor_id], references: [id])
|
||||
donor_id Int?
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// Auth
|
||||
// -----------------
|
||||
model GeneratedK1 {
|
||||
value String @id
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -22,3 +22,25 @@ functions:
|
||||
path: graphql
|
||||
method: get
|
||||
cors: true
|
||||
login:
|
||||
handler: functions/auth/login.handler
|
||||
events:
|
||||
- http:
|
||||
path: auth/login
|
||||
method: post
|
||||
cors: true
|
||||
- http:
|
||||
path: auth/login
|
||||
method: get
|
||||
cors: true
|
||||
logout:
|
||||
handler: functions/auth/logout.handler
|
||||
events:
|
||||
- http:
|
||||
path: auth/logout
|
||||
method: post
|
||||
cors: true
|
||||
- http:
|
||||
path: auth/logout
|
||||
method: get
|
||||
cors: true
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Wallet_Service } from "./services";
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { useWrapperSetup } from "./utils/Wrapper";
|
||||
import LoadingPage from "./Components/LoadingPage/LoadingPage";
|
||||
import LoginPage from "./features/Auth/pages/LoginPage";
|
||||
|
||||
// Pages
|
||||
const FeedPage = React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage"))
|
||||
@@ -56,6 +57,8 @@ function App() {
|
||||
|
||||
<Route path="/donate" element={<DonatePage />} />
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
<Route path="/" element={<Navigate to="/products" />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ControlledMenu,
|
||||
} from '@szhsin/react-menu';
|
||||
import '@szhsin/react-menu/dist/index.css';
|
||||
import { FiAward, FiChevronDown, FiFeather, FiMic } from "react-icons/fi";
|
||||
import { FiAward, FiChevronDown, FiFeather, FiLogIn, FiMic } from "react-icons/fi";
|
||||
|
||||
|
||||
export default function NavDesktop() {
|
||||
@@ -137,6 +137,7 @@ export default function NavDesktop() {
|
||||
</ul>
|
||||
|
||||
<div className="ml-auto"></div>
|
||||
|
||||
<motion.div
|
||||
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
|
||||
className="flex"
|
||||
@@ -160,7 +161,9 @@ export default function NavDesktop() {
|
||||
<BsSearch className='scale-125 text-gray-400' />
|
||||
</IconButton>}
|
||||
</motion.div>
|
||||
|
||||
<Link to='/login' className="ml-16 font-bold hover:text-primary-800 hover:underline">
|
||||
Login <FiLogIn />
|
||||
</Link>
|
||||
<div className="relative h-24">
|
||||
<motion.div
|
||||
initial={{
|
||||
|
||||
86
src/features/Auth/pages/LoginPage.tsx
Normal file
86
src/features/Auth/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { BsFillLightningChargeFill } from "react-icons/bs";
|
||||
import { Grid } from "react-loader-spinner";
|
||||
import { useMeQuery } from "src/graphql"
|
||||
|
||||
|
||||
export default function LoginPage() {
|
||||
const [loadingLnurl, setLoadingLnurl] = useState(true)
|
||||
const [lnurlAuth, setLnurlAuth] = useState("");
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isPollilng, setIsPollilng] = useState(false)
|
||||
|
||||
|
||||
|
||||
const meQuery = useMeQuery({
|
||||
onCompleted: (data) => {
|
||||
const stateChanged = Boolean(data.me) !== isLoggedIn;
|
||||
if (stateChanged)
|
||||
setIsPollilng(false);
|
||||
|
||||
setIsLoggedIn(Boolean(data.me));
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch(process.env.REACT_APP_AUTH_END_POINT! + '/login')
|
||||
const data = await res.json()
|
||||
setLoadingLnurl(false);
|
||||
setLnurlAuth(data.encoded)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const { startPolling, stopPolling } = meQuery;
|
||||
useEffect(() => {
|
||||
if (isPollilng)
|
||||
startPolling(1500);
|
||||
else
|
||||
stopPolling();
|
||||
}, [isPollilng, startPolling, stopPolling])
|
||||
|
||||
|
||||
const onLogin = () => {
|
||||
setIsPollilng(true);
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await fetch(process.env.REACT_APP_AUTH_END_POINT! + '/logout', {
|
||||
method: "GET",
|
||||
'credentials': "include"
|
||||
})
|
||||
setIsPollilng(true);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-[80vh] flex flex-col justify-center items-center">
|
||||
{loadingLnurl && <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>}
|
||||
{!loadingLnurl &&
|
||||
(isLoggedIn ?
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<h3 className="text-body4">
|
||||
Hello: <span className="font-bold">@{meQuery.data?.me?.name.slice(0, 10)}...</span>
|
||||
</h3>
|
||||
<img src={meQuery.data?.me?.avatar} className='w-80 h-80 object-cover' alt="" />
|
||||
<button
|
||||
onClick={logout}
|
||||
className="text-black font-bolder bg-gray-200 rounded-12 px-16 py-12 mt-36 active:scale-90 transition-transform">
|
||||
{isPollilng ? "Logging you out..." : "Logout"}
|
||||
</button>
|
||||
</div> :
|
||||
<a
|
||||
href={lnurlAuth}
|
||||
onClick={onLogin}
|
||||
className='text-black font-bolder bg-yellow-200 hover:bg-yellow-300 rounded-12 px-16 py-12 active:scale-90 transition-transform'>
|
||||
Login with Lightning <BsFillLightningChargeFill className="scale-125" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/features/Auth/pages/me.graphql
Normal file
7
src/features/Auth/pages/me.graphql
Normal file
@@ -0,0 +1,7 @@
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,7 @@ export type Query = {
|
||||
getProject: Project;
|
||||
getTrendingPosts: Array<Post>;
|
||||
hottestProjects: Array<Project>;
|
||||
me: Maybe<User>;
|
||||
newProjects: Array<Project>;
|
||||
popularTopics: Array<Topic>;
|
||||
projectsByCategory: Array<Project>;
|
||||
@@ -350,6 +351,11 @@ export type SearchProjectsQueryVariables = Exact<{
|
||||
|
||||
export type SearchProjectsQuery = { __typename?: 'Query', searchProjects: Array<{ __typename?: 'Project', id: number, thumbnail_image: string, title: string, category: { __typename?: 'Category', title: string, id: number } }> };
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MeQuery = { __typename?: 'Query', me: { __typename?: 'User', id: number, name: string, avatar: string } | null };
|
||||
|
||||
export type DonationsStatsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -536,6 +542,42 @@ export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
|
||||
export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQuery>;
|
||||
export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>;
|
||||
export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>;
|
||||
export const MeDocument = gql`
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMeQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMeQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
||||
export const DonationsStatsDocument = gql`
|
||||
query DonationsStats {
|
||||
getDonationsStats {
|
||||
|
||||
@@ -4,11 +4,12 @@ import { RetryLink } from "@apollo/client/link/retry";
|
||||
|
||||
let apiClientUri = '/.netlify/functions/graphql';
|
||||
|
||||
if (process.env.REACT_APP_API_END_POINT)
|
||||
apiClientUri = process.env.REACT_APP_API_END_POINT
|
||||
if (process.env.REACT_APP_GRAPHQL_END_POINT)
|
||||
apiClientUri = process.env.REACT_APP_GRAPHQL_END_POINT
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: apiClientUri
|
||||
uri: apiClientUri,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||
|
||||
Reference in New Issue
Block a user