mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
feat: rebuild lnurl-auth api using sessions & passport, add login with QR
This commit is contained in:
@@ -2,7 +2,13 @@ const lnurl = require('lnurl')
|
||||
const crypto = require('crypto')
|
||||
const { prisma } = require('../../prisma')
|
||||
const { CONSTS } = require('../../utils')
|
||||
|
||||
const express = require('express');
|
||||
const session = require("express-session");
|
||||
const passport = require("passport");
|
||||
const lnurlAuth = require("passport-lnurl-auth");
|
||||
const assert = require('assert');
|
||||
const { HttpError } = require('lnurl/lib');
|
||||
var SQLiteStore = require('connect-sqlite3')(session);
|
||||
|
||||
async function generateSecret() {
|
||||
let secret = null
|
||||
@@ -28,10 +34,11 @@ function isHashUsed(hash) {
|
||||
return prisma.generatedK1.findFirst({ where: { value: hash } })
|
||||
}
|
||||
|
||||
function addHash(hash) {
|
||||
function addHash(hash, sid) {
|
||||
return prisma.generatedK1.create({
|
||||
data: {
|
||||
value: hash,
|
||||
sid,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -57,11 +64,10 @@ function removeExpiredHashes() {
|
||||
})
|
||||
}
|
||||
|
||||
async function generateAuthUrl() {
|
||||
async function generateAuthUrl(sid) {
|
||||
const hostname = CONSTS.LNURL_AUTH_HOST;
|
||||
console.log(hostname);
|
||||
const secret = await generateSecret()
|
||||
await addHash(createHash(secret))
|
||||
await addHash(createHash(secret), sid)
|
||||
const url = `${hostname}?tag=login&k1=${secret}`
|
||||
return {
|
||||
url,
|
||||
@@ -70,6 +76,16 @@ async function generateAuthUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getSidByK1(k1) {
|
||||
const hash = createHash(k1)
|
||||
const data = await prisma.generatedK1.findFirst({
|
||||
where: {
|
||||
value: hash,
|
||||
}
|
||||
});
|
||||
return data.sid;
|
||||
}
|
||||
|
||||
async function verifySig(sig, k1, key) {
|
||||
if (!lnurl.verifyAuthorizationSignature(sig, k1, key)) {
|
||||
const message = 'Signature verification failed'
|
||||
@@ -94,10 +110,143 @@ function createHash(data) {
|
||||
return crypto.createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function setupAuthMiddelwares(app) {
|
||||
app.use(session({
|
||||
secret: "12345",
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
store: new SQLiteStore()
|
||||
}));
|
||||
|
||||
passport.use(
|
||||
new lnurlAuth.Strategy(function (linkingPublicKey, done) {
|
||||
const user = { id: linkingPublicKey };
|
||||
console.log("Strategy Function");
|
||||
console.log(user);
|
||||
// let user = map.user.get(linkingPublicKey);
|
||||
// if (!user) {
|
||||
// user = { id: linkingPublicKey };
|
||||
// map.user.set(linkingPublicKey, user);
|
||||
// }
|
||||
done(null, user);
|
||||
})
|
||||
);
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
app.use(passport.authenticate("lnurl-auth"));
|
||||
passport.serializeUser(function (user, done) {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function (id, done) {
|
||||
|
||||
// done(null, map.user.get(id) || null);
|
||||
done(null, id || null);
|
||||
});
|
||||
return app;
|
||||
/*
|
||||
app.get(
|
||||
"/do-login",
|
||||
function (req, res, next) {
|
||||
next();
|
||||
},
|
||||
async function (req, res) {
|
||||
|
||||
|
||||
if (req.query.k1 || req.query.key || req.query.sig) {
|
||||
// Check signature against provided linking public key.
|
||||
// This request could originate from a mobile app (ie. not their browser).
|
||||
let session;
|
||||
assert.ok(
|
||||
req.query.k1,
|
||||
new HttpError('Missing required parameter: "k1"', 400)
|
||||
);
|
||||
assert.ok(
|
||||
req.query.sig,
|
||||
new HttpError('Missing required parameter: "sig"', 400)
|
||||
);
|
||||
assert.ok(
|
||||
req.query.key,
|
||||
new HttpError('Missing required parameter: "key"', 400)
|
||||
);
|
||||
session = map.session.get(req.query.k1);
|
||||
assert.ok(
|
||||
session,
|
||||
new HttpError("Secret does not match any known session", 400)
|
||||
);
|
||||
const { k1, sig, key } = req.query;
|
||||
assert.ok(
|
||||
verifyAuthorizationSignature(sig, k1, key),
|
||||
new HttpError("Invalid signature", 400)
|
||||
);
|
||||
session.lnurlAuth = session.lnurlAuth || {};
|
||||
session.lnurlAuth.linkingPublicKey = req.query.key;
|
||||
|
||||
|
||||
const result = await session.save();
|
||||
console.log(result);
|
||||
res.status(200).json({ status: "OK" });
|
||||
}
|
||||
|
||||
req.session = req.session || {};
|
||||
req.session.lnurlAuth = req.session.lnurlAuth || {};
|
||||
let k1 = req.session.lnurlAuth.k1 || null;
|
||||
if (!k1) {
|
||||
k1 = req.session.lnurlAuth.k1 = generateSecret(32, "hex");
|
||||
map.session.set(k1, req.session);
|
||||
}
|
||||
|
||||
const callbackUrl =
|
||||
"https://" +
|
||||
`${req.get("host")}/do-login?${querystring.stringify({
|
||||
k1,
|
||||
tag: "login",
|
||||
})}`;
|
||||
|
||||
const encoded = lnurl.encode(callbackUrl).toUpperCase();
|
||||
const qrCode = await qrcode.toDataURL(encoded);
|
||||
return res.json({
|
||||
lnurl: encoded,
|
||||
qrCode: qrCode,
|
||||
});
|
||||
}
|
||||
);
|
||||
*/
|
||||
|
||||
// app.get("/logout", function (req, res, next) {
|
||||
// if (req.user) {
|
||||
// req.session.destroy();
|
||||
// return res.redirect("/");
|
||||
// }
|
||||
// next();
|
||||
// });
|
||||
|
||||
// app.get("/me", function (req, res, next) {
|
||||
// res.json({ user: req.user ? req.user : null });
|
||||
|
||||
// next();
|
||||
// });
|
||||
|
||||
// app.get("/profile", function (req, res, next) {
|
||||
// if (!req.user) {
|
||||
// return res.redirect("/login");
|
||||
// }
|
||||
|
||||
// res.render("profile", { user: req.user });
|
||||
|
||||
// next();
|
||||
// });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateAuthUrl: generateAuthUrl,
|
||||
verifySig: verifySig,
|
||||
removeHash: removeHash,
|
||||
createHash: createHash,
|
||||
removeExpiredHashes: removeExpiredHashes
|
||||
removeExpiredHashes: removeExpiredHashes,
|
||||
getSidByK1: getSidByK1
|
||||
}
|
||||
@@ -1,31 +1,18 @@
|
||||
const { ApolloServer } = require("apollo-server-lambda");
|
||||
const schema = require('./schema')
|
||||
const cookie = require('cookie')
|
||||
const jose = require('jose');
|
||||
const { CONSTS } = require('../utils');
|
||||
|
||||
const extractKey = async (cookieHeader) => {
|
||||
const cookies = cookie.parse(cookieHeader ?? '');
|
||||
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 payload.pubKey
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const { createExpressApp } = require('../utils/express-app')
|
||||
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
context: async ({ event }) => {
|
||||
const userPubKey = await extractKey(event.headers.cookie ?? event.headers.Cookie)
|
||||
context: async ({ event, context, express }) => {
|
||||
const userPubKey = express.req.user?.id;
|
||||
return { userPubKey }
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
const apolloHandler = server.createHandler({
|
||||
expressGetMiddlewareOptions: {
|
||||
cors: {
|
||||
@@ -33,6 +20,11 @@ const apolloHandler = server.createHandler({
|
||||
credentials: true,
|
||||
}
|
||||
},
|
||||
expressAppFromMiddleware(middleware) {
|
||||
const app = createExpressApp();
|
||||
app.use(middleware)
|
||||
return app;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -5,19 +5,23 @@ const cookie = require('cookie')
|
||||
const jose = require('jose');
|
||||
const { CONSTS } = require('../utils');
|
||||
const { CORS_HEADERS } = require('../utils/consts');
|
||||
const serverless = require('serverless-http');
|
||||
// const { expressApp } = require('../utils/express-app');
|
||||
// const { sessionsStore } = require('../utils/sessionsStore');
|
||||
const { getSidByK1 } = require('../auth/services/lnurl.service');
|
||||
const express = require('express')
|
||||
|
||||
const session = require("express-session");
|
||||
const passport = require("passport");
|
||||
const lnurlAuth = require("passport-lnurl-auth");
|
||||
// const { sessionsStore } = require('./sessionsStore');
|
||||
var cors = require('cors');
|
||||
const { createExpressApp } = require('../utils/express-app');
|
||||
const { sessionsStore } = require('../utils/sessionsStore');
|
||||
|
||||
async function generateAuthUrl() {
|
||||
const data = await LnurlService.generateAuthUrl();
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: CORS_HEADERS,
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
}
|
||||
async function login(req, res) {
|
||||
|
||||
|
||||
async function login(tag, k1, sig, key) {
|
||||
const { tag, k1, sig, key } = req.query;
|
||||
if (tag !== 'login') {
|
||||
return {
|
||||
statusCode: 400,
|
||||
@@ -99,19 +103,92 @@ async function login(tag, k1, sig, key) {
|
||||
}
|
||||
}
|
||||
|
||||
const app = createExpressApp();
|
||||
|
||||
app.get('/login', async (req, res) => {
|
||||
const { tag, k1, sig, key } = req.query;
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
const { tag, k1, sig, key } = event.queryStringParameters ?? {}
|
||||
|
||||
if (event.httpMethod !== "GET") {
|
||||
return { statusCode: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
// Generate an auth URL
|
||||
if (!sig || !key) {
|
||||
return generateAuthUrl();
|
||||
|
||||
const data = await LnurlService.generateAuthUrl(req.sessionID);
|
||||
return res.status(200).json(data);
|
||||
}
|
||||
else {
|
||||
return login(tag, k1, sig, key)
|
||||
if (tag !== 'login')
|
||||
return res.status(400).send("Invalid tag provided")
|
||||
|
||||
// Verify login params
|
||||
try {
|
||||
await LnurlService.verifySig(sig, k1, key)
|
||||
} catch (error) {
|
||||
return res.status(400).json({ status: 'ERROR', reason: 'Invalid Signature' })
|
||||
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
//Create user if not already existing
|
||||
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`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Update the session with the secret
|
||||
const sid = await getSidByK1(k1);
|
||||
const d = await new Promise((res, rej) => {
|
||||
sessionsStore.get(sid, (err, d) => {
|
||||
if (err) rej(err);
|
||||
res(d)
|
||||
})
|
||||
});
|
||||
// console.log(d);
|
||||
await new Promise((res, rej) => {
|
||||
sessionsStore.set(sid, { ...d, lnurlAuth: { linkingPublicKey: key } }, (err) => {
|
||||
if (err) rej(err);
|
||||
res()
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
LnurlService.removeHash(LnurlService.createHash(k1)).catch();
|
||||
LnurlService.removeExpiredHashes().catch();
|
||||
|
||||
return res.status(200).json({ status: "OK" })
|
||||
|
||||
// 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,
|
||||
// sameSite: 'none',
|
||||
// })
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return res.status(200).json({ status: 'ERROR', reason: 'Unexpected error happened, please try again' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handler = serverless(app);
|
||||
exports.handler = async (event, context) => {
|
||||
return await handler(event, context);
|
||||
};
|
||||
@@ -1,24 +1,16 @@
|
||||
const serverless = require('serverless-http');
|
||||
const { createExpressApp } = require('../utils/express-app');
|
||||
const app = createExpressApp();
|
||||
|
||||
const cookie = require('cookie');
|
||||
const { CORS_HEADERS } = require('../utils/consts');
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
const myCookie = cookie.serialize('Authorization', '', {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
maxAge: -1,
|
||||
})
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
body: JSON.stringify({
|
||||
status: 'OK',
|
||||
}),
|
||||
'headers': {
|
||||
'Set-Cookie': myCookie,
|
||||
'Cache-Control': 'no-cache',
|
||||
...CORS_HEADERS
|
||||
}
|
||||
app.get('/logout', (req, res, next) => {
|
||||
if (req.user) {
|
||||
req.session.destroy();
|
||||
return res.redirect("/");
|
||||
}
|
||||
next();
|
||||
})
|
||||
|
||||
const handler = serverless(app);
|
||||
exports.handler = async (event, context) => {
|
||||
return await handler(event, context);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
const LNURL_AUTH_HOST = process.env.LNURL_AUTH_HOST
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET
|
||||
|
||||
const CORS_HEADERS = {
|
||||
'Access-Control-Allow-Origin': 'http://localhost:3000',
|
||||
@@ -13,7 +14,8 @@ const CONSTS = {
|
||||
JWT_SECRET,
|
||||
BOLT_FUN_LIGHTNING_ADDRESS,
|
||||
LNURL_AUTH_HOST,
|
||||
CORS_HEADERS
|
||||
CORS_HEADERS,
|
||||
SESSION_SECRET
|
||||
}
|
||||
|
||||
module.exports = CONSTS;
|
||||
51
functions/utils/express-app.js
Normal file
51
functions/utils/express-app.js
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
const express = require('express');
|
||||
const session = require("express-session");
|
||||
const passport = require("passport");
|
||||
const lnurlAuth = require("passport-lnurl-auth");
|
||||
const { sessionsStore } = require('./sessionsStore');
|
||||
var cors = require('cors');
|
||||
const { SESSION_SECRET } = require('./consts');
|
||||
|
||||
const createExpressApp = () => {
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: ['http://localhost:3000', 'https://studio.apollographql.com'],
|
||||
credentials: true,
|
||||
}))
|
||||
|
||||
app.use(session({
|
||||
secret: SESSION_SECRET,
|
||||
resave: false,
|
||||
store: sessionsStore,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new lnurlAuth.Strategy(function (linkingPublicKey, done) {
|
||||
// The user has successfully authenticated using lnurl-auth.
|
||||
// The linked public key is provided here.
|
||||
// You can use this as a unique reference for the user similar to a username or email address.
|
||||
const user = { id: linkingPublicKey };
|
||||
done(null, user);
|
||||
}));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
app.use(passport.authenticate("lnurl-auth"));
|
||||
passport.serializeUser(function (user, done) {
|
||||
done(null, user?.id);
|
||||
});
|
||||
passport.deserializeUser(function (id, done) {
|
||||
done(null, { id } || null);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = { createExpressApp };
|
||||
16
functions/utils/sessionsStore.js
Normal file
16
functions/utils/sessionsStore.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
const session = require("express-session");
|
||||
var Store = require('connect-pg-simple')(session);
|
||||
let sessionsStore;
|
||||
|
||||
if (!global.sessionsStore) {
|
||||
console.log("New Sessions Store");
|
||||
global.sessionsStore = new Store({
|
||||
createTableIfMissing: true,
|
||||
tableName: "user_sessions",
|
||||
|
||||
});
|
||||
}
|
||||
sessionsStore = global.sessionsStore;
|
||||
|
||||
module.exports = { sessionsStore };
|
||||
1541
package-lock.json
generated
1541
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -23,11 +23,18 @@
|
||||
"apollo-server": "^3.6.7",
|
||||
"apollo-server-lambda": "^3.6.7",
|
||||
"axios": "^0.26.1",
|
||||
"better-sqlite3": "^7.5.3",
|
||||
"better-sqlite3-session-store": "^0.0.3",
|
||||
"chance": "^1.1.8",
|
||||
"connect-pg-simple": "^7.0.0",
|
||||
"connect-sqlite3": "^0.9.13",
|
||||
"cookie": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"express": "^4.18.1",
|
||||
"express-session": "^1.17.3",
|
||||
"framer-motion": "^6.3.0",
|
||||
"fslightbox-react": "^1.6.2-2",
|
||||
"graphql": "^16.3.0",
|
||||
@@ -41,7 +48,10 @@
|
||||
"marked": "^4.0.14",
|
||||
"nexus": "^1.3.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-lnurl-auth": "^1.5.0",
|
||||
"prisma": "^3.12.0",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react-accessible-accordion": "^5.0.0",
|
||||
"react-confetti": "^6.0.1",
|
||||
@@ -66,6 +76,7 @@
|
||||
"react-topbar-progress-indicator": "^4.1.1",
|
||||
"remirror": "^1.0.77",
|
||||
"secp256k1": "^4.0.3",
|
||||
"serverless-http": "^3.0.1",
|
||||
"typescript": "^4.6.3",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webln": "^0.3.0",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "GeneratedK1" ADD COLUMN "sid" TEXT;
|
||||
@@ -220,5 +220,6 @@ model Donation {
|
||||
// -----------------
|
||||
model GeneratedK1 {
|
||||
value String @id
|
||||
sid String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
useDotenv: true
|
||||
|
||||
custom:
|
||||
serverless-offline:
|
||||
httpPort: 8888
|
||||
@@ -17,30 +19,24 @@ functions:
|
||||
- http:
|
||||
path: graphql
|
||||
method: post
|
||||
cors: true
|
||||
- http:
|
||||
path: graphql
|
||||
method: get
|
||||
cors: true
|
||||
login:
|
||||
handler: functions/login/login.handler
|
||||
events:
|
||||
- http:
|
||||
path: login
|
||||
method: post
|
||||
cors: true
|
||||
- http:
|
||||
path: login
|
||||
method: get
|
||||
cors: true
|
||||
logout:
|
||||
handler: functions/logout/logout.handler
|
||||
events:
|
||||
- http:
|
||||
path: logout
|
||||
method: post
|
||||
cors: true
|
||||
- http:
|
||||
path: logout
|
||||
method: get
|
||||
cors: true
|
||||
|
||||
@@ -5,11 +5,16 @@ import { Grid } from "react-loader-spinner";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMeQuery } from "src/graphql"
|
||||
import { CONSTS } from "src/utils";
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { IoQrCode } from "react-icons/io5";
|
||||
import Button from "src/Components/Button/Button";
|
||||
|
||||
|
||||
|
||||
const getLnurlAuth = async () => {
|
||||
const res = await fetch(CONSTS.apiEndpoint + '/login')
|
||||
const res = await fetch(CONSTS.apiEndpoint + '/login', {
|
||||
credentials: 'include'
|
||||
})
|
||||
const data = await res.json()
|
||||
return data;
|
||||
}
|
||||
@@ -17,6 +22,7 @@ const getLnurlAuth = async () => {
|
||||
|
||||
export default function LoginPage() {
|
||||
const [loadingLnurl, setLoadingLnurl] = useState(true)
|
||||
const [showQr, setShowQr] = useState(false)
|
||||
const [lnurlAuth, setLnurlAuth] = useState("");
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [error, setError] = useState(null)
|
||||
@@ -49,11 +55,13 @@ export default function LoginPage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onLogin = () => {
|
||||
const startPolling = () => {
|
||||
meQuery.startPolling(1500)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let content = <></>
|
||||
|
||||
if (error)
|
||||
@@ -92,10 +100,20 @@ export default function LoginPage() {
|
||||
</p>
|
||||
<a
|
||||
href={lnurlAuth}
|
||||
onClick={onLogin}
|
||||
onClick={startPolling}
|
||||
className='block 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" />
|
||||
Login with WebLN <BsFillLightningChargeFill className="scale-125" />
|
||||
</a>
|
||||
|
||||
<div className="text-gray-500 text-body5 font-bold">OR</div>
|
||||
{!showQr && <button className="text-blue-500 text-body4 px-12 py-8 hover:bg-gray-100 rounded-8" onClick={() => { setShowQr(true); startPolling() }}>
|
||||
Scan QR <IoQrCode width={'100%'} />
|
||||
</button>}
|
||||
{showQr && <QRCodeSVG
|
||||
width={160}
|
||||
height={160}
|
||||
value={lnurlAuth}
|
||||
/>}
|
||||
</div>;
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user