Skip to content

Commit 49cb2c3

Browse files
committed
limit number of invites user can generate
1 parent 68e485f commit 49cb2c3

5 files changed

Lines changed: 95 additions & 22 deletions

File tree

api/src/controllers/user.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export const register = async (req, res) => {
5454
}
5555

5656
await Invite.findOneAndUpdate({ _id: id }, { $set: { claimed: true } })
57+
await User.findOneAndUpdate(
58+
{ _id: invite.invitingUser },
59+
{ $inc: { remainingInvites: -1 } }
60+
)
5761
} catch (err) {
5862
res.status(500).send(`Error verifying invitation: ${err.message}`)
5963
return
@@ -79,6 +83,7 @@ export const register = async (req, res) => {
7983
created,
8084
role,
8185
invitedBy: invite?.invitingUser,
86+
remainingInvites: 0,
8287
})
8388

8489
newUser.uid = crypto
@@ -94,6 +99,7 @@ export const register = async (req, res) => {
9499
token: jwt.sign(
95100
{
96101
id: newUser._id,
102+
username: newUser.username,
97103
created,
98104
role,
99105
},
@@ -130,6 +136,7 @@ export const login = async (req, res) => {
130136
token: jwt.sign(
131137
{
132138
id: user._id,
139+
username: user.username,
133140
created: user.created,
134141
role: user.role,
135142
},
@@ -154,6 +161,12 @@ export const login = async (req, res) => {
154161
}
155162

156163
export const generateInvite = async (req, res) => {
164+
const user = await User.findOne({ _id: req.userId }).lean()
165+
166+
if (user.remainingInvites < 1) {
167+
res.status(403).send('You do not have any remaining invites')
168+
}
169+
157170
const created = Date.now()
158171
const validUntil = created + 48 * 60 * 60 * 1000
159172

@@ -322,6 +335,7 @@ export const fetchUser = async (req, res) => {
322335
username: 1,
323336
created: 1,
324337
...(req.userRole === 'admin' ? { email: 1, invitedBy: 1 } : {}),
338+
remainingInvites: 1,
325339
},
326340
},
327341
{

api/src/schema/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const User = new mongoose.Schema({
1010
banned: Boolean,
1111
role: String,
1212
invitedBy: mongoose.Schema.ObjectId,
13+
remainingInvites: Number,
1314
})
1415

1516
export default mongoose.model('user', User)

api/src/setup/createAdminUser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const createAdminUser = async () => {
1414
role: 'admin',
1515
password: hash,
1616
created,
17+
remainingInvites: Number.MAX_SAFE_INTEGER,
1718
})
1819
adminUser.uid = crypto
1920
.createHash('sha256')

client/components/Button.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const StyledButton = styled.button(
2424
bg: darken(0.1, theme.colors.primary),
2525
borderColor: darken(0.1, theme.colors.primary),
2626
},
27+
'&[disabled]': {
28+
cursor: 'not-allowed',
29+
opacity: 0.5,
30+
'&:hover': {
31+
borderColor: 'primary',
32+
},
33+
'&:focus, &:active': {
34+
bg: 'primary',
35+
},
36+
},
2737
}),
2838
({ theme }) =>
2939
variant({
@@ -39,6 +49,14 @@ const StyledButton = styled.button(
3949
bg: darken(0.1, theme.colors.sidebar),
4050
borderColor: darken(0.1, theme.colors.sidebar),
4151
},
52+
'&[disabled]': {
53+
'&:hover': {
54+
borderColor: 'sidebar',
55+
},
56+
'&:focus, &:active': {
57+
bg: 'sidebar',
58+
},
59+
},
4260
},
4361
noBackground: {
4462
bg: 'transparent',
@@ -58,4 +76,12 @@ const StyledButton = styled.button(
5876
space
5977
)
6078

61-
export default StyledButton
79+
const Button = ({ onClick, disabled, ...rest }) => (
80+
<StyledButton
81+
onClick={disabled ? undefined : onClick}
82+
disabled={disabled}
83+
{...rest}
84+
/>
85+
)
86+
87+
export default Button

client/pages/account.js

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Select from '../components/Select'
1515
import Button from '../components/Button'
1616
import List from '../components/List'
1717

18-
const Account = ({ token, invites = [], userRole }) => {
18+
const Account = ({ token, invites = [], user, userRole }) => {
1919
const [invitesList, setInvitesList] = useState(invites)
2020

2121
const {
@@ -92,14 +92,27 @@ const Account = ({ token, invites = [], userRole }) => {
9292
>
9393
<Text as="h2">Invites</Text>
9494
<form onSubmit={handleGenerateInvite}>
95-
<Box display="flex" alignItems="center">
95+
<Box
96+
display="flex"
97+
alignItems="center"
98+
border="1px solid"
99+
borderColor="border"
100+
borderRadius={1}
101+
p={2}
102+
pl={4}
103+
>
104+
<Text color="grey" mr={4}>
105+
{user.remainingInvites} remaining
106+
</Text>
96107
{userRole === 'admin' && (
97108
<Select name="role" required mr={3}>
98109
<option value="user">Role: user</option>
99110
<option value="admin">Role: admin</option>
100111
</Select>
101112
)}
102-
<Button>Generate invite</Button>
113+
<Button disabled={user.remainingInvites < 1}>
114+
Generate invite
115+
</Button>
103116
</Box>
104117
</form>
105118
</Box>
@@ -126,7 +139,13 @@ const Account = ({ token, invites = [], userRole }) => {
126139
header: 'Valid until',
127140
accessor: 'validUntil',
128141
cell: ({ value }) => (
129-
<Text>{moment(value).format('HH:mm Do MMM YYYY')}</Text>
142+
<Text
143+
style={{
144+
textDecoration: value < Date.now() ? 'line-through' : 'none',
145+
}}
146+
>
147+
{moment(value).format('HH:mm Do MMM YYYY')}
148+
</Text>
130149
),
131150
gridWidth: '1.2fr',
132151
},
@@ -148,21 +167,25 @@ const Account = ({ token, invites = [], userRole }) => {
148167
},
149168
{
150169
header: 'Copy',
151-
cell: ({ row }) => (
152-
<Button
153-
variant="secondary"
154-
onClick={() => {
155-
copy(
156-
`${location.protocol}//${location.host}/register?token=${row.token}`
157-
)
158-
alert('Invite link copied to clipboard')
159-
}}
160-
px={1}
161-
py={1}
162-
>
163-
<Copy size={24} />
164-
</Button>
165-
),
170+
cell: ({ row }) => {
171+
console.log(row)
172+
return (
173+
<Button
174+
variant="secondary"
175+
onClick={() => {
176+
copy(
177+
`${location.protocol}//${location.host}/register?token=${row.token}`
178+
)
179+
alert('Invite link copied to clipboard')
180+
}}
181+
disabled={row.claimed}
182+
px={1}
183+
py={1}
184+
>
185+
<Copy size={24} />
186+
</Button>
187+
)
188+
},
166189
rightAlign: true,
167190
gridWidth: '42px',
168191
},
@@ -184,6 +207,7 @@ const Account = ({ token, invites = [], userRole }) => {
184207
name="newPassword"
185208
type="password"
186209
label="New password"
210+
autoComplete="new-password"
187211
mb={4}
188212
required
189213
/>
@@ -203,17 +227,24 @@ export const getServerSideProps = async ({ req }) => {
203227
serverRuntimeConfig: { SQ_JWT_SECRET },
204228
} = getConfig()
205229

206-
const { role } = jwt.verify(token, SQ_JWT_SECRET)
230+
const { role, username } = jwt.verify(token, SQ_JWT_SECRET)
207231

208232
try {
233+
const userRes = await fetch(`${SQ_API_URL}/user/${username}`, {
234+
headers: {
235+
'Content-Type': 'application/json',
236+
Authorization: `Bearer ${token}`,
237+
},
238+
})
239+
const user = await userRes.json()
209240
const invitesRes = await fetch(`${SQ_API_URL}/account/invites`, {
210241
headers: {
211242
'Content-Type': 'application/json',
212243
Authorization: `Bearer ${token}`,
213244
},
214245
})
215246
const invites = await invitesRes.json()
216-
return { props: { invites, userRole: role } }
247+
return { props: { invites, user, userRole: role } }
217248
} catch (e) {
218249
return { props: {} }
219250
}

0 commit comments

Comments
 (0)