2021-03-25 19:29:24 +00:00
|
|
|
|
import NextAuth from 'next-auth'
|
|
|
|
|
import Providers from 'next-auth/providers'
|
2022-06-02 22:55:23 +00:00
|
|
|
|
import { PrismaLegacyAdapter } from '../../../lib/prisma-adapter'
|
2021-03-25 19:29:24 +00:00
|
|
|
|
import prisma from '../../../api/models'
|
2022-03-10 22:47:00 +00:00
|
|
|
|
import nodemailer from 'nodemailer'
|
2022-06-02 22:55:23 +00:00
|
|
|
|
import { getSession } from 'next-auth/client'
|
2021-03-25 19:29:24 +00:00
|
|
|
|
|
2022-12-19 22:27:52 +00:00
|
|
|
|
export default (req, res) => NextAuth(req, res, {
|
2021-06-27 03:09:39 +00:00
|
|
|
|
callbacks: {
|
|
|
|
|
/**
|
|
|
|
|
* @param {object} token Decrypted JSON Web Token
|
|
|
|
|
* @param {object} user User object (only available on sign in)
|
|
|
|
|
* @param {object} account Provider account (only available on sign in)
|
|
|
|
|
* @param {object} profile Provider profile (only available on sign in)
|
|
|
|
|
* @param {boolean} isNewUser True if new user (only available on sign in)
|
|
|
|
|
* @return {object} JSON Web Token that will be saved
|
|
|
|
|
*/
|
|
|
|
|
async jwt (token, user, account, profile, isNewUser) {
|
|
|
|
|
// Add additional session params
|
|
|
|
|
if (user?.id) {
|
2022-06-02 22:55:23 +00:00
|
|
|
|
token.id = Number(user.id)
|
|
|
|
|
// HACK next-auth needs this to do account linking with jwts
|
|
|
|
|
// see: https://github.com/nextauthjs/next-auth/issues/625
|
|
|
|
|
token.user = { id: Number(user.id) }
|
2021-06-27 03:09:39 +00:00
|
|
|
|
}
|
2021-10-15 17:56:54 +00:00
|
|
|
|
|
2022-12-19 22:27:52 +00:00
|
|
|
|
if (isNewUser) {
|
|
|
|
|
// if referrer exists, set on user
|
|
|
|
|
if (req.cookies.sn_referrer && user?.id) {
|
|
|
|
|
const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
|
|
|
|
|
if (referrer) {
|
|
|
|
|
await prisma.user.update({ where: { id: user.id }, data: { referrerId: referrer.id } })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sign them up for the newsletter
|
|
|
|
|
if (profile.email) {
|
|
|
|
|
fetch(process.env.LIST_MONK_URL + '/api/subscribers', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
Authorization: 'Basic ' + Buffer.from(process.env.LIST_MONK_AUTH).toString('base64')
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
email: profile.email,
|
|
|
|
|
name: 'blank',
|
|
|
|
|
lists: [2],
|
|
|
|
|
status: 'enabled',
|
|
|
|
|
preconfirm_subscriptions: true
|
|
|
|
|
})
|
|
|
|
|
}).then(async r => console.log(await r.json())).catch(console.log)
|
|
|
|
|
}
|
2022-05-04 18:29:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-27 03:09:39 +00:00
|
|
|
|
return token
|
|
|
|
|
},
|
|
|
|
|
async session (session, token) {
|
|
|
|
|
// we need to add additional session params here
|
2022-06-02 22:55:23 +00:00
|
|
|
|
session.user.id = Number(token.id)
|
2021-06-27 03:09:39 +00:00
|
|
|
|
return session
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-03-25 19:29:24 +00:00
|
|
|
|
providers: [
|
2021-06-27 03:09:39 +00:00
|
|
|
|
Providers.Credentials({
|
2023-01-18 18:49:20 +00:00
|
|
|
|
id: 'lightning',
|
|
|
|
|
// The name to display on the sign in form (e.g. 'Sign in with...')
|
2021-06-27 03:09:39 +00:00
|
|
|
|
name: 'Lightning',
|
|
|
|
|
// The credentials is used to generate a suitable form on the sign in page.
|
|
|
|
|
// You can specify whatever fields you are expecting to be submitted.
|
|
|
|
|
// e.g. domain, username, password, 2FA token, etc.
|
|
|
|
|
credentials: {
|
|
|
|
|
pubkey: { label: 'publickey', type: 'text' },
|
|
|
|
|
k1: { label: 'k1', type: 'text' }
|
|
|
|
|
},
|
|
|
|
|
async authorize (credentials, req) {
|
|
|
|
|
const { k1, pubkey } = credentials
|
|
|
|
|
try {
|
|
|
|
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
2023-01-18 18:49:20 +00:00
|
|
|
|
await prisma.lnAuth.delete({ where: { k1 } })
|
2021-06-27 03:09:39 +00:00
|
|
|
|
if (lnauth.pubkey === pubkey) {
|
|
|
|
|
let user = await prisma.user.findUnique({ where: { pubkey } })
|
2022-06-02 22:55:23 +00:00
|
|
|
|
const session = await getSession({ req })
|
2021-06-27 03:09:39 +00:00
|
|
|
|
if (!user) {
|
2022-06-02 22:55:23 +00:00
|
|
|
|
// if we are logged in, update rather than create
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
user = await prisma.user.update({ where: { id: session.user.id }, data: { pubkey } })
|
|
|
|
|
} else {
|
|
|
|
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), pubkey } })
|
|
|
|
|
}
|
|
|
|
|
} else if (session && session.user?.id !== user.id) {
|
|
|
|
|
throw new Error('account not linked')
|
2021-06-27 03:09:39 +00:00
|
|
|
|
}
|
2022-06-02 22:55:23 +00:00
|
|
|
|
|
2023-01-18 18:49:20 +00:00
|
|
|
|
return user
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
Providers.Credentials({
|
|
|
|
|
id: 'slashtags',
|
|
|
|
|
// The name to display on the sign in form (e.g. 'Sign in with...')
|
|
|
|
|
name: 'Slashtags',
|
|
|
|
|
// The credentials is used to generate a suitable form on the sign in page.
|
|
|
|
|
// You can specify whatever fields you are expecting to be submitted.
|
|
|
|
|
// e.g. domain, username, password, 2FA token, etc.
|
|
|
|
|
credentials: {
|
|
|
|
|
pubkey: { label: 'publickey', type: 'text' },
|
|
|
|
|
k1: { label: 'k1', type: 'text' }
|
|
|
|
|
},
|
|
|
|
|
async authorize (credentials, req) {
|
|
|
|
|
const { k1, pubkey } = credentials
|
|
|
|
|
try {
|
|
|
|
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
|
|
|
|
await prisma.lnAuth.delete({ where: { k1 } })
|
|
|
|
|
if (lnauth.pubkey === pubkey) {
|
|
|
|
|
let user = await prisma.user.findUnique({ where: { slashtagId: pubkey } })
|
|
|
|
|
const session = await getSession({ req })
|
|
|
|
|
if (!user) {
|
|
|
|
|
// if we are logged in, update rather than create
|
|
|
|
|
if (session?.user) {
|
|
|
|
|
user = await prisma.user.update({ where: { id: session.user.id }, data: { slashtagId: pubkey } })
|
|
|
|
|
} else {
|
|
|
|
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), slashtagId: pubkey } })
|
|
|
|
|
}
|
|
|
|
|
} else if (session && session.user?.id !== user.id) {
|
|
|
|
|
throw new Error('account not linked')
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-27 03:09:39 +00:00
|
|
|
|
return user
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}),
|
2021-03-25 19:29:24 +00:00
|
|
|
|
Providers.GitHub({
|
|
|
|
|
clientId: process.env.GITHUB_ID,
|
2021-05-21 19:34:40 +00:00
|
|
|
|
clientSecret: process.env.GITHUB_SECRET,
|
2023-03-13 17:40:25 +00:00
|
|
|
|
authorization: 'https://github.com/login/oauth/authorize',
|
|
|
|
|
scope: '', // read-only acces to public information
|
2021-05-21 19:34:40 +00:00
|
|
|
|
profile: profile => {
|
|
|
|
|
return {
|
|
|
|
|
...profile,
|
|
|
|
|
name: profile.login
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-24 21:05:07 +00:00
|
|
|
|
}),
|
|
|
|
|
Providers.Twitter({
|
|
|
|
|
clientId: process.env.TWITTER_ID,
|
2021-05-21 19:34:40 +00:00
|
|
|
|
clientSecret: process.env.TWITTER_SECRET,
|
|
|
|
|
profile: profile => {
|
|
|
|
|
return {
|
|
|
|
|
...profile,
|
|
|
|
|
name: profile.screen_name
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-25 19:29:24 +00:00
|
|
|
|
}),
|
|
|
|
|
Providers.Email({
|
2021-06-02 14:23:30 +00:00
|
|
|
|
server: process.env.LOGIN_EMAIL_SERVER,
|
|
|
|
|
from: process.env.LOGIN_EMAIL_FROM,
|
2022-03-10 22:47:00 +00:00
|
|
|
|
sendVerificationRequest,
|
2021-05-21 19:34:40 +00:00
|
|
|
|
profile: profile => {
|
|
|
|
|
return profile
|
|
|
|
|
}
|
2021-03-25 19:29:24 +00:00
|
|
|
|
})
|
|
|
|
|
],
|
2022-06-02 22:55:23 +00:00
|
|
|
|
adapter: PrismaLegacyAdapter({ prisma }),
|
2021-06-27 03:18:32 +00:00
|
|
|
|
secret: process.env.NEXTAUTH_SECRET,
|
2021-06-27 03:09:39 +00:00
|
|
|
|
session: { jwt: true },
|
|
|
|
|
jwt: {
|
|
|
|
|
signingKey: process.env.JWT_SIGNING_PRIVATE_KEY
|
|
|
|
|
},
|
2021-04-24 21:05:07 +00:00
|
|
|
|
pages: {
|
2023-01-10 23:13:37 +00:00
|
|
|
|
signIn: '/login',
|
|
|
|
|
verifyRequest: '/email'
|
2021-04-24 21:05:07 +00:00
|
|
|
|
}
|
2022-12-19 22:27:52 +00:00
|
|
|
|
})
|
2022-03-10 22:47:00 +00:00
|
|
|
|
|
|
|
|
|
function sendVerificationRequest ({
|
|
|
|
|
identifier: email,
|
|
|
|
|
url,
|
|
|
|
|
token,
|
|
|
|
|
baseUrl,
|
|
|
|
|
provider
|
|
|
|
|
}) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const { server, from } = provider
|
|
|
|
|
// Strip protocol from URL and use domain as site name
|
|
|
|
|
const site = baseUrl.replace(/^https?:\/\//, '')
|
|
|
|
|
|
|
|
|
|
nodemailer.createTransport(server).sendMail(
|
|
|
|
|
{
|
|
|
|
|
to: email,
|
|
|
|
|
from,
|
|
|
|
|
subject: `login to ${site}`,
|
|
|
|
|
text: text({ url, site, email }),
|
|
|
|
|
html: html({ url, site, email })
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
|
|
|
|
if (error) {
|
|
|
|
|
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
|
|
|
|
|
}
|
|
|
|
|
return resolve()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Email HTML body
|
|
|
|
|
const html = ({ url, site, email }) => {
|
|
|
|
|
// Insert invisible space into domains and email address to prevent both the
|
|
|
|
|
// email address and the domain from being turned into a hyperlink by email
|
|
|
|
|
// clients like Outlook and Apple mail, as this is confusing because it seems
|
|
|
|
|
// like they are supposed to click on their email address to sign in.
|
|
|
|
|
const escapedEmail = `${email.replace(/\./g, '​.')}`
|
|
|
|
|
const escapedSite = `${site.replace(/\./g, '​.')}`
|
|
|
|
|
|
|
|
|
|
// Some simple styling options
|
|
|
|
|
const backgroundColor = '#f5f5f5'
|
|
|
|
|
const textColor = '#212529'
|
|
|
|
|
const mainBackgroundColor = '#ffffff'
|
|
|
|
|
const buttonBackgroundColor = '#FADA5E'
|
|
|
|
|
const buttonBorderColor = '#FADA5E'
|
|
|
|
|
const buttonTextColor = '#212529'
|
|
|
|
|
|
|
|
|
|
// Uses tables for layout and inline CSS due to email client limitations
|
|
|
|
|
return `
|
|
|
|
|
<body style="background: ${backgroundColor};">
|
|
|
|
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
|
|
|
|
<strong>${escapedSite}</strong>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
|
|
|
|
login as <strong>${escapedEmail}</strong>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="padding: 20px 0;">
|
|
|
|
|
<table border="0" cellspacing="0" cellpadding="0">
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">login</a></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
|
|
|
|
Or copy and paste this link: <a href="#" style="text-decoration:none; color:${textColor}">${url}</a>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 10px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
|
|
|
|
If you did not request this email you can safely ignore it.
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
</body>
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Email text body –fallback for email clients that don't render HTML
|
|
|
|
|
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
|