finish up invites
This commit is contained in:
parent
3a52f8967a
commit
955d1aa1b2
|
@ -10,6 +10,16 @@ export default {
|
||||||
return await models.invite.findMany({
|
return await models.invite.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: me.id
|
userId: me.id
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
invite: async (parent, { id }, { me, models }) => {
|
||||||
|
return await models.invite.findUnique({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,6 +54,13 @@ export default {
|
||||||
Invite: {
|
Invite: {
|
||||||
invitees: async (invite, args, { me, models }) => {
|
invitees: async (invite, args, { me, models }) => {
|
||||||
return await models.user.findMany({ where: { inviteId: invite.id } })
|
return await models.user.findMany({ where: { inviteId: invite.id } })
|
||||||
|
},
|
||||||
|
user: async (invite, args, { me, models }) => {
|
||||||
|
return await models.user.findUnique({ where: { id: invite.userId } })
|
||||||
|
},
|
||||||
|
poor: async (invite, args, { me, models }) => {
|
||||||
|
const user = await models.user.findUnique({ where: { id: invite.userId } })
|
||||||
|
return user.msats < invite.gift * 1000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,12 @@ async function serialize (models, call) {
|
||||||
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
|
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
|
||||||
bail(new Error('withdrawal invoice exists and is pending'))
|
bail(new Error('withdrawal invoice exists and is pending'))
|
||||||
}
|
}
|
||||||
|
if (error.message.includes('SN_INELIGIBLE')) {
|
||||||
|
bail(new Error('user ineligible for gift'))
|
||||||
|
}
|
||||||
|
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
|
||||||
|
bail(new Error('faucet has been revoked or is exhausted'))
|
||||||
|
}
|
||||||
if (error.message.includes('40001')) {
|
if (error.message.includes('40001')) {
|
||||||
throw new Error('wallet balance serialization failure - retry again')
|
throw new Error('wallet balance serialization failure - retry again')
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,12 @@ export default {
|
||||||
bio: async (user, args, { models }) => {
|
bio: async (user, args, { models }) => {
|
||||||
return getItem(user, { id: user.bioId }, { models })
|
return getItem(user, { id: user.bioId }, { models })
|
||||||
},
|
},
|
||||||
|
hasInvites: async (user, args, { models }) => {
|
||||||
|
const anInvite = await models.invite.findFirst({
|
||||||
|
where: { userId: user.id }
|
||||||
|
})
|
||||||
|
return !!anInvite
|
||||||
|
},
|
||||||
hasNewNotes: async (user, args, { models }) => {
|
hasNewNotes: async (user, args, { models }) => {
|
||||||
// check if any votes have been cast for them since checkedNotesAt
|
// check if any votes have been cast for them since checkedNotesAt
|
||||||
const votes = await models.$queryRaw(`
|
const votes = await models.$queryRaw(`
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro'
|
||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
invites: [Invite!]!
|
invites: [Invite!]!
|
||||||
|
invite(id: ID!): Invite
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
|
@ -16,6 +17,8 @@ export default gql`
|
||||||
invitees: [User!]!
|
invitees: [User!]!
|
||||||
gift: Int!
|
gift: Int!
|
||||||
limit: Int
|
limit: Int
|
||||||
|
user: User!
|
||||||
revoked: Boolean!
|
revoked: Boolean!
|
||||||
|
poor: Boolean!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default gql`
|
||||||
freePosts: Int!
|
freePosts: Int!
|
||||||
freeComments: Int!
|
freeComments: Int!
|
||||||
hasNewNotes: Boolean!
|
hasNewNotes: Boolean!
|
||||||
|
hasInvites: Boolean!
|
||||||
tipDefault: Int!
|
tipDefault: Int!
|
||||||
bio: Item
|
bio: Item
|
||||||
sats: Int!
|
sats: Int!
|
||||||
|
|
|
@ -64,7 +64,12 @@ export default function Header () {
|
||||||
</Link>
|
</Link>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
<Link href='/invites' passHref>
|
<Link href='/invites' passHref>
|
||||||
<NavDropdown.Item>invites</NavDropdown.Item>
|
<NavDropdown.Item>invites
|
||||||
|
{me && !me.hasInvites &&
|
||||||
|
<div className='p-1 d-inline-block bg-success ml-1'>
|
||||||
|
<span className='invisible'>{' '}</span>
|
||||||
|
</div>}
|
||||||
|
</NavDropdown.Item>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const EmailSchema = Yup.object({
|
||||||
email: Yup.string().email('email is no good').required('required').trim()
|
email: Yup.string().email('email is no good').required('required').trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function Login ({ providers, callbackUrl, error }) {
|
export default function Login ({ providers, callbackUrl, error, Header }) {
|
||||||
const errors = {
|
const errors = {
|
||||||
Signin: 'Try signing with a different account.',
|
Signin: 'Try signing with a different account.',
|
||||||
OAuthSignin: 'Try signing with a different account.',
|
OAuthSignin: 'Try signing with a different account.',
|
||||||
|
@ -37,6 +37,7 @@ export default function Login ({ providers, callbackUrl, error }) {
|
||||||
return (
|
return (
|
||||||
<LayoutCenter noFooter>
|
<LayoutCenter noFooter>
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
|
{Header && <Header />}
|
||||||
{errorMessage &&
|
{errorMessage &&
|
||||||
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
|
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
|
||||||
{router.query.type === 'lightning'
|
{router.query.type === 'lightning'
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function MeProvider ({ children }) {
|
||||||
bio {
|
bio {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
hasInvites
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000 })
|
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||||
|
|
|
@ -11,5 +11,10 @@ export const INVITE_FIELDS = gql`
|
||||||
gift
|
gift
|
||||||
limit
|
limit
|
||||||
revoked
|
revoked
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
poor
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,11 +1,42 @@
|
||||||
import Login from '../../components/login'
|
import Login from '../../components/login'
|
||||||
import { providers, getSession } from 'next-auth/client'
|
import { providers, getSession } from 'next-auth/client'
|
||||||
|
import models from '../../api/models'
|
||||||
|
import serialize from '../../api/resolvers/serial'
|
||||||
|
import { gql } from '@apollo/client'
|
||||||
|
import { INVITE_FIELDS } from '../../fragments/invites'
|
||||||
|
import getSSRApolloClient from '../../api/ssrApollo'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
export async function getServerSideProps ({ req, res, query: { id, error = null } }) {
|
||||||
const session = await getSession({ req })
|
const session = await getSession({ req })
|
||||||
|
|
||||||
|
const client = await getSSRApolloClient(req)
|
||||||
|
const { data } = await client.query({
|
||||||
|
query: gql`
|
||||||
|
${INVITE_FIELDS}
|
||||||
|
{
|
||||||
|
invite(id: "${id}") {
|
||||||
|
...InviteFields
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data?.invite) {
|
||||||
|
return {
|
||||||
|
notFound: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (session && res) {
|
if (session && res) {
|
||||||
// send down the userid and the invite to the db
|
try {
|
||||||
|
// attempt to send gift
|
||||||
|
// catch any errors and just ignore them for now
|
||||||
|
await serialize(models,
|
||||||
|
models.$queryRaw('SELECT invite_drain($1, $2)', session.user.id, id))
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(302, {
|
res.writeHead(302, {
|
||||||
Location: '/'
|
Location: '/'
|
||||||
})
|
})
|
||||||
|
@ -17,9 +48,36 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
|
||||||
props: {
|
props: {
|
||||||
providers: await providers({ req, res }),
|
providers: await providers({ req, res }),
|
||||||
callbackUrl: process.env.SELF_URL + req.url,
|
callbackUrl: process.env.SELF_URL + req.url,
|
||||||
|
invite: data.invite,
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login
|
function InviteHeader ({ invite }) {
|
||||||
|
console.log(invite.poor)
|
||||||
|
let Inner
|
||||||
|
if (invite.revoked) {
|
||||||
|
Inner = () => <div className='text-danger'>this invite link expired</div>
|
||||||
|
} else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
|
||||||
|
Inner = () => <div className='text-danger'>this invite link has no more sats</div>
|
||||||
|
} else {
|
||||||
|
Inner = () => (
|
||||||
|
<div>
|
||||||
|
get <span className='text-success'>{invite.gift} free sats</span> from{' '}
|
||||||
|
<Link href={`/${invite.user.name}`} passHref><a>@{invite.user.name}</a></Link>{' '}
|
||||||
|
when you sign up today
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2 className='text-center pb-3'>
|
||||||
|
<Inner />
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Invite ({ invite, ...props }) {
|
||||||
|
return <Login Header={() => <InviteHeader invite={invite} />} {...props} />
|
||||||
|
}
|
||||||
|
|
|
@ -46,10 +46,10 @@ function InviteForm () {
|
||||||
limit: undefined
|
limit: undefined
|
||||||
}}
|
}}
|
||||||
schema={InviteSchema}
|
schema={InviteSchema}
|
||||||
onSubmit={async ({ limit, ...values }) => {
|
onSubmit={async ({ limit, gift }) => {
|
||||||
const { error } = await createInvite({
|
const { error } = await createInvite({
|
||||||
variables: {
|
variables: {
|
||||||
...values, limit: limit ? Number(limit) : limit
|
gift: Number(gift), limit: limit ? Number(limit) : limit
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -95,6 +95,8 @@ function Invite ({ invite, active }) {
|
||||||
placeholder={`https://stacker.news/invites/${invite.id}`} readOnly
|
placeholder={`https://stacker.news/invites/${invite.id}`} readOnly
|
||||||
/>
|
/>
|
||||||
<div className={styles.other}>
|
<div className={styles.other}>
|
||||||
|
<span>{invite.gift} sat gift</span>
|
||||||
|
<span> \ </span>
|
||||||
<span>{invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''}</span>
|
<span>{invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''}</span>
|
||||||
{active
|
{active
|
||||||
? (
|
? (
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
inviter_id INTEGER;
|
||||||
|
inviter_sats INTEGER;
|
||||||
|
gift INTEGER;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
-- check user was created in last hour
|
||||||
|
-- check user did not already redeem an invite
|
||||||
|
PERFORM FROM users
|
||||||
|
WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR'
|
||||||
|
AND users."inviteId" IS NULL;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'SN_INELIGIBLE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- check that invite has not reached limit
|
||||||
|
-- check that invite is not revoked
|
||||||
|
SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite"
|
||||||
|
LEFT JOIN users ON users."inviteId" = invite_id
|
||||||
|
WHERE "Invite".id = invite_id AND NOT "Invite".revoked
|
||||||
|
GROUP BY "Invite".id
|
||||||
|
HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- check that inviter has sufficient balance
|
||||||
|
SELECT (msats / 1000) INTO inviter_sats
|
||||||
|
FROM users WHERE id = inviter_id;
|
||||||
|
IF inviter_sats < gift THEN
|
||||||
|
RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- subtract amount from inviter
|
||||||
|
UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id;
|
||||||
|
-- add amount to invitee
|
||||||
|
UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id WHERE id = user_id;
|
||||||
|
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;
|
Loading…
Reference in New Issue