Skip to content

Commit cdb8e23

Browse files
committed
invite via email
1 parent c3f0c15 commit cdb8e23

5 files changed

Lines changed: 140 additions & 75 deletions

File tree

api/src/controllers/torrent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import Comment from '../schema/comment'
99
import { hexToBinary } from '../middleware/announce'
1010

1111
export const embellishTorrentsWithTrackerScrape = async (torrents) => {
12+
if (!torrents.length) return []
13+
1214
const infoHashes = torrents.map((torrent) => hexToBinary(torrent.infoHash))
1315
const query = qs.stringify(
1416
{ info_hash: infoHashes },

api/src/controllers/user.js

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const sendVerificationEmail = async (address, token) => {
1111
await mail.sendMail({
1212
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
1313
to: address,
14-
subject: 'Verify you email address',
14+
subject: 'Verify your email address',
1515
text: `Thank you for joining ${process.env.SQ_SITE_NAME}. Please follow the link below to verify your email address.
1616
1717
${process.env.SQ_BASE_URL}/verify-email?token=${token}`,
@@ -47,7 +47,7 @@ export const register = async (req, res) => {
4747
const { id } = decoded
4848

4949
invite = await Invite.findOne({ _id: id }).lean()
50-
const { claimed, validUntil, invitingUser } = invite
50+
const { claimed, validUntil, invitingUser, email } = invite
5151

5252
if (claimed) {
5353
res.status(403).send('Invitation has already been claimed')
@@ -59,17 +59,18 @@ export const register = async (req, res) => {
5959
return
6060
}
6161

62+
if (email !== req.body.email) {
63+
res
64+
.status(403)
65+
.send('Email address does not match invited email address')
66+
return
67+
}
68+
6269
const inviter = User.findOne({ _id: invitingUser }).lean()
6370
if (!inviter || inviter.banned) {
6471
res.status(403).send('Inviting user doesn’t exist or has been banned')
6572
return
6673
}
67-
68-
await Invite.findOneAndUpdate({ _id: id }, { $set: { claimed: true } })
69-
await User.findOneAndUpdate(
70-
{ _id: invite.invitingUser },
71-
{ $inc: { remainingInvites: -1 } }
72-
)
7374
} catch (err) {
7475
res.status(500).send(`Error verifying invitation: ${err.message}`)
7576
return
@@ -118,6 +119,22 @@ export const register = async (req, res) => {
118119
await sendVerificationEmail(req.body.email, emailVerificationToken)
119120

120121
if (createdUser) {
122+
if (req.body.invite) {
123+
const decoded = jwt.verify(
124+
req.body.invite,
125+
process.env.SQ_JWT_SECRET
126+
)
127+
const { id } = decoded
128+
await Invite.findOneAndUpdate(
129+
{ _id: id },
130+
{ $set: { claimed: true } }
131+
)
132+
await User.findOneAndUpdate(
133+
{ _id: invite.invitingUser },
134+
{ $inc: { remainingInvites: -1 } }
135+
)
136+
}
137+
121138
res.send({
122139
token: jwt.sign(
123140
{
@@ -186,34 +203,47 @@ export const login = async (req, res) => {
186203
}
187204

188205
export const generateInvite = async (req, res) => {
189-
const user = await User.findOne({ _id: req.userId }).lean()
206+
if (req.body.email && req.body.role) {
207+
const user = await User.findOne({ _id: req.userId }).lean()
190208

191-
if (user.remainingInvites < 1) {
192-
res.status(403).send('You do not have any remaining invites')
193-
}
209+
if (user.remainingInvites < 1) {
210+
res.status(403).send('You do not have any remaining invites')
211+
}
194212

195-
const created = Date.now()
196-
const validUntil = created + 48 * 60 * 60 * 1000
213+
const created = Date.now()
214+
const validUntil = created + 48 * 60 * 60 * 1000
197215

198-
const { role } = req.query
216+
const { email, role } = req.body
199217

200-
const invite = new Invite({
201-
invitingUser: req.userId,
202-
created,
203-
validUntil,
204-
claimed: false,
205-
role: role || 'user',
206-
})
218+
const invite = new Invite({
219+
invitingUser: req.userId,
220+
created,
221+
validUntil,
222+
claimed: false,
223+
email,
224+
role: role || 'user',
225+
})
207226

208-
invite.token = jwt.sign(
209-
{ id: invite._id, validUntil },
210-
process.env.SQ_JWT_SECRET
211-
)
227+
invite.token = jwt.sign(
228+
{ id: invite._id, validUntil },
229+
process.env.SQ_JWT_SECRET
230+
)
212231

213-
const createdInvite = await invite.save()
232+
const createdInvite = await invite.save()
214233

215-
if (createdInvite) {
216-
res.send(createdInvite)
234+
if (createdInvite) {
235+
mail.sendMail({
236+
from: `"${process.env.SQ_SITE_NAME}" <${process.env.SQ_MAIL_FROM_ADDRESS}>`,
237+
to: email,
238+
subject: 'Invite',
239+
text: `You have been invited to join ${process.env.SQ_SITE_NAME}. Please follow the link below to register.
240+
241+
${process.env.SQ_BASE_URL}/register?token=${createdInvite.token}`,
242+
})
243+
res.send(createdInvite)
244+
}
245+
} else {
246+
res.status(400).send('Request must include email, role')
217247
}
218248
}
219249

api/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ app.use(auth)
144144

145145
// user/account routes
146146
app.get('/account/invites', fetchInvites)
147-
app.get('/account/generate-invite', generateInvite)
147+
app.post('/account/generate-invite', generateInvite)
148148
app.post('/account/change-password', changePassword)
149149
app.get('/account/get-role', getUserRole)
150150
app.get('/account/get-verified', getUserVerifiedEmailStatus)

api/src/schema/invite.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Invite = new mongoose.Schema({
66
validUntil: Number,
77
claimed: Boolean,
88
token: String,
9+
email: String,
910
role: String,
1011
})
1112

client/pages/account.js

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import Select from '../components/Select'
1414
import Button from '../components/Button'
1515
import List from '../components/List'
1616
import { NotificationContext } from '../components/Notifications'
17+
import Modal from '../components/Modal'
1718

1819
const Account = ({ token, invites = [], user, userRole }) => {
1920
const [invitesList, setInvitesList] = useState(invites)
21+
const [showInviteModal, setShowInviteModal] = useState(false)
2022

2123
const { addNotification } = useContext(NotificationContext)
2224

@@ -29,16 +31,17 @@ const Account = ({ token, invites = [], user, userRole }) => {
2931
const form = new FormData(e.target)
3032

3133
try {
32-
const inviteRes = await fetch(
33-
`${SQ_API_URL}/account/generate-invite?role=${
34-
form.get('role') || 'user'
35-
}`,
36-
{
37-
headers: {
38-
Authorization: `Bearer ${token}`,
39-
},
40-
}
41-
)
34+
const inviteRes = await fetch(`${SQ_API_URL}/account/generate-invite`, {
35+
method: 'POST',
36+
headers: {
37+
'Content-Type': 'application/json',
38+
Authorization: `Bearer ${token}`,
39+
},
40+
body: JSON.stringify({
41+
role: form.get('role') || 'user',
42+
email: form.get('email'),
43+
}),
44+
})
4245

4346
if (inviteRes.status !== 200) {
4447
const reason = await inviteRes.text()
@@ -52,9 +55,11 @@ const Account = ({ token, invites = [], user, userRole }) => {
5255
return currentInvitesList
5356
})
5457

55-
addNotification('success', 'Invite generated successfully')
58+
addNotification('success', 'Invite sent successfully')
59+
60+
setShowInviteModal(false)
5661
} catch (e) {
57-
addNotification('error', `Could not generate invite: ${e.message}`)
62+
addNotification('error', `Could not send invite: ${e.message}`)
5863
console.error(e)
5964
}
6065
}
@@ -109,43 +114,42 @@ const Account = ({ token, invites = [], user, userRole }) => {
109114
mb={4}
110115
>
111116
<Text as="h2">Invites</Text>
112-
<form onSubmit={handleGenerateInvite}>
113-
<Box
114-
display="flex"
115-
alignItems="center"
116-
border="1px solid"
117-
borderColor="border"
118-
borderRadius={1}
119-
p={2}
120-
pl={4}
117+
<Box
118+
display="flex"
119+
alignItems="center"
120+
border="1px solid"
121+
borderColor="border"
122+
borderRadius={1}
123+
p={2}
124+
pl={4}
125+
>
126+
<Text color="grey" mr={4}>
127+
{user.remainingInvites || 0} remaining
128+
</Text>
129+
<Button
130+
onClick={() => setShowInviteModal(true)}
131+
disabled={(user.remainingInvites || 0) < 1}
121132
>
122-
<Text color="grey" mr={4}>
123-
{user.remainingInvites || 0} remaining
124-
</Text>
125-
{userRole === 'admin' && (
126-
<Select name="role" required mr={3}>
127-
<option value="user">Role: user</option>
128-
<option value="admin">Role: admin</option>
129-
</Select>
130-
)}
131-
<Button disabled={(user.remainingInvites || 0) < 1}>
132-
Generate invite
133-
</Button>
134-
</Box>
135-
</form>
133+
Send invite
134+
</Button>
135+
</Box>
136136
</Box>
137137
<List
138138
data={invitesList}
139139
columns={[
140140
{
141-
header: 'Token',
142-
accessor: 'token',
143-
cell: ({ value }) => (
144-
<Text fontFamily="monospace">
145-
...{value.slice(value.length - 16)}
141+
header: 'Email',
142+
accessor: 'email',
143+
cell: ({ value, row }) => (
144+
<Text
145+
_css={{
146+
textDecoration: row.claimed ? 'line-through' : 'none',
147+
}}
148+
>
149+
{value}
146150
</Text>
147151
),
148-
gridWidth: '1fr',
152+
gridWidth: '1.75fr',
149153
},
150154
{
151155
header: 'Claimed',
@@ -158,7 +162,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
158162
accessor: 'validUntil',
159163
cell: ({ value }) => (
160164
<Text
161-
style={{
165+
_css={{
162166
textDecoration: value < Date.now() ? 'line-through' : 'none',
163167
}}
164168
>
@@ -173,7 +177,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
173177
cell: ({ value }) => (
174178
<Text>{moment(value).format('HH:mm Do MMM YYYY')}</Text>
175179
),
176-
gridWidth: '1fr',
180+
gridWidth: '1.2fr',
177181
},
178182
{
179183
header: 'Role',
@@ -184,7 +188,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
184188
gridWidth: '0.6fr',
185189
},
186190
{
187-
header: 'Copy',
191+
header: 'Copy link',
188192
cell: ({ row }) => {
189193
return (
190194
<Button
@@ -207,7 +211,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
207211
)
208212
},
209213
rightAlign: true,
210-
gridWidth: '42px',
214+
gridWidth: '80px',
211215
},
212216
]}
213217
mb={5}
@@ -233,6 +237,34 @@ const Account = ({ token, invites = [], user, userRole }) => {
233237
/>
234238
<Button>Change password</Button>
235239
</form>
240+
{showInviteModal && (
241+
<Modal close={() => setShowInviteModal(false)}>
242+
<Text mb={5}>
243+
Enter an email address to send an invite. The invited user will need
244+
to sign up with the same email address. Once the invite is
245+
generated, you can also copy a direct invite link.
246+
</Text>
247+
<form onSubmit={handleGenerateInvite}>
248+
<Input name="email" type="email" label="Email" mb={4} required />
249+
{userRole === 'admin' && (
250+
<Select name="role" mb={4} required>
251+
<option value="user">Role: user</option>
252+
<option value="admin">Role: admin</option>
253+
</Select>
254+
)}
255+
<Box display="flex" justifyContent="flex-end">
256+
<Button
257+
onClick={() => setShowInviteModal(false)}
258+
variant="secondary"
259+
mr={3}
260+
>
261+
Cancel
262+
</Button>
263+
<Button>Send invite</Button>
264+
</Box>
265+
</form>
266+
</Modal>
267+
)}
236268
</>
237269
)
238270
}

0 commit comments

Comments
 (0)