Skip to content

Commit 3de1c4a

Browse files
committed
adds user roles, default admin account, invite specific roles, record inviting user
1 parent 8994587 commit 3de1c4a

9 files changed

Lines changed: 152 additions & 52 deletions

File tree

api/src/controllers/user.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export const register = async (req, res) => {
1616
}
1717

1818
if (req.body.username && req.body.email && req.body.password) {
19+
let invite
20+
1921
if (process.env.SQ_ALLOW_REGISTER === 'invite') {
2022
if (!req.body.invite) {
2123
res
@@ -25,12 +27,14 @@ export const register = async (req, res) => {
2527
)
2628
return
2729
}
30+
}
2831

32+
if (req.body.invite) {
2933
try {
3034
const decoded = jwt.verify(req.body.invite, process.env.SQ_JWT_SECRET)
3135
const { id } = decoded
3236

33-
const invite = await Invite.findOne({ _id: id }).lean()
37+
invite = await Invite.findOne({ _id: id }).lean()
3438
const { claimed, validUntil, invitingUser } = invite
3539

3640
if (claimed) {
@@ -65,13 +69,16 @@ export const register = async (req, res) => {
6569

6670
if (!user) {
6771
const hash = await bcrypt.hash(req.body.password, 10)
72+
const role = invite?.role || 'user'
6873

6974
const newUser = new User({
7075
username: req.body.username,
7176
email: req.body.email,
7277
password: hash,
7378
torrents: {},
7479
created,
80+
role,
81+
invitedBy: invite?.invitingUser,
7582
})
7683

7784
newUser.uid = crypto
@@ -83,7 +90,8 @@ export const register = async (req, res) => {
8390
newUser.token = jwt.sign(
8491
{
8592
id: newUser._id,
86-
created: created,
93+
created,
94+
role,
8795
},
8896
process.env.SQ_JWT_SECRET
8997
)
@@ -144,11 +152,14 @@ export const generateInvite = async (req, res) => {
144152
const created = Date.now()
145153
const validUntil = created + 48 * 60 * 60 * 1000
146154

155+
const { role } = req.query
156+
147157
const invite = new Invite({
148158
invitingUser: req.userId,
149159
created,
150160
validUntil,
151161
claimed: false,
162+
role: role || 'user',
152163
})
153164

154165
invite.token = jwt.sign(

api/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
listLatest,
2626
searchTorrents,
2727
} from './controllers/torrent'
28+
import createAdminUser from './setup/createAdminUser'
2829

2930
const connectToDb = () => {
3031
console.log('[sq] initiating db connection...')
@@ -43,6 +44,7 @@ connectToDb()
4344

4445
mongoose.connection.once('open', () => {
4546
console.log('[sq] connected to mongodb successfully')
47+
createAdminUser()
4648
})
4749

4850
const app = express()

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+
role: String,
910
})
1011

1112
export default mongoose.model('invite', Invite)

api/src/schema/user.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const User = new mongoose.Schema({
99
token: String,
1010
created: Number,
1111
banned: Boolean,
12+
role: String,
13+
invitedBy: mongoose.Schema.ObjectId,
1214
})
1315

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

api/src/setup/createAdminUser.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import bcrypt from 'bcrypt'
2+
import crypto from 'crypto'
3+
import jwt from 'jsonwebtoken'
4+
import User from '../schema/user'
5+
6+
const createAdminUser = async () => {
7+
const existingAdmin = await User.findOne({ username: 'admin' }).lean()
8+
if (!existingAdmin) {
9+
const created = Date.now()
10+
const hash = await bcrypt.hash('admin', 10)
11+
const adminUser = new User({
12+
username: 'admin',
13+
email: 'admin@sqtracker',
14+
role: 'admin',
15+
password: hash,
16+
created,
17+
})
18+
adminUser.uid = crypto
19+
.createHash('sha256')
20+
.update(adminUser._id.toString())
21+
.digest('hex')
22+
.slice(0, 10)
23+
adminUser.token = jwt.sign(
24+
{
25+
id: adminUser._id,
26+
created,
27+
role: 'admin',
28+
},
29+
process.env.SQ_JWT_SECRET
30+
)
31+
await adminUser.save()
32+
}
33+
}
34+
35+
export default createAdminUser

client/components/Infobox.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react'
2+
import Box from './Box'
3+
4+
const Infobox = ({ children, ...rest }) => (
5+
<Box
6+
bg="sidebar"
7+
border="1px solid"
8+
borderColor="border"
9+
borderRadius={1}
10+
p={4}
11+
{...rest}
12+
>
13+
{children}
14+
</Box>
15+
)
16+
17+
export default Infobox

client/pages/account.js

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,41 @@ import React, { useState } from 'react'
22
import getConfig from 'next/config'
33
import moment from 'moment'
44
import copy from 'copy-to-clipboard'
5+
import jwt from 'jsonwebtoken'
56
import { Copy } from '@styled-icons/boxicons-regular/Copy'
67
import withAuth from '../utils/withAuth'
78
import getReqCookies from '../utils/getReqCookies'
89
import SEO from '../components/SEO'
910
import Box from '../components/Box'
1011
import Text from '../components/Text'
12+
import Infobox from '../components/Infobox'
1113
import Input from '../components/Input'
14+
import Select from '../components/Select'
1215
import Button from '../components/Button'
1316
import List from '../components/List'
1417

15-
const Account = ({ token, invites }) => {
18+
const Account = ({ token, invites, userRole }) => {
1619
const [invitesList, setInvitesList] = useState(invites)
1720

1821
const {
1922
publicRuntimeConfig: { SQ_API_URL },
2023
} = getConfig()
2124

22-
const handleGenerateInvite = async () => {
25+
const handleGenerateInvite = async (e) => {
26+
e.preventDefault()
27+
const form = new FormData(e.target)
28+
2329
try {
24-
const inviteRes = await fetch(`${SQ_API_URL}/account/generate-invite`, {
25-
headers: {
26-
Authorization: `Bearer ${token}`,
27-
},
28-
})
30+
const inviteRes = await fetch(
31+
`${SQ_API_URL}/account/generate-invite?role=${
32+
form.get('role') || 'user'
33+
}`,
34+
{
35+
headers: {
36+
Authorization: `Bearer ${token}`,
37+
},
38+
}
39+
)
2940
const invite = await inviteRes.json()
3041
setInvitesList((cur) => {
3142
const currentInvitesList = [...cur]
@@ -68,51 +79,75 @@ const Account = ({ token, invites }) => {
6879
<Text as="h1" mb={5}>
6980
My account
7081
</Text>
82+
{userRole === 'admin' && (
83+
<Infobox mb={5}>
84+
<Text>This is an admin account.</Text>
85+
</Infobox>
86+
)}
7187
<Box
7288
display="flex"
7389
alignItems="center"
7490
justifyContent="space-between"
7591
mb={4}
7692
>
7793
<Text as="h2">Invites</Text>
78-
<Button onClick={handleGenerateInvite}>Generate invite</Button>
94+
<form onSubmit={handleGenerateInvite}>
95+
<Box display="flex" alignItems="center">
96+
{userRole === 'admin' && (
97+
<Select name="role" required mr={3}>
98+
<option value="user">Role: user</option>
99+
<option value="admin">Role: admin</option>
100+
</Select>
101+
)}
102+
<Button>Generate invite</Button>
103+
</Box>
104+
</form>
79105
</Box>
80106
<List
81107
data={invitesList}
82108
columns={[
83109
{
110+
header: 'Token',
84111
accessor: 'token',
85112
cell: ({ value }) => (
86113
<Text fontFamily="monospace">
87-
{value.slice(0, 10)}...{value.slice(value.length - 10)}
114+
...{value.slice(value.length - 16)}
88115
</Text>
89116
),
90117
gridWidth: '1fr',
91118
},
92119
{
120+
header: 'Claimed',
93121
accessor: 'claimed',
94-
cell: ({ value }) => (
95-
<Text>{value ? 'Claimed' : 'Not claimed'}</Text>
96-
),
122+
cell: ({ value }) => <Text>{value ? 'Yes' : 'No'}</Text>,
97123
gridWidth: '0.5fr',
98124
},
99125
{
126+
header: 'Valid until',
100127
accessor: 'validUntil',
101128
cell: ({ value }) => (
102-
<Text>
103-
Valid until {moment(value).format('HH:mm Do MMM YYYY')}
104-
</Text>
129+
<Text>{moment(value).format('HH:mm Do MMM YYYY')}</Text>
105130
),
106131
gridWidth: '1.2fr',
107132
},
108133
{
134+
header: 'Created',
109135
accessor: 'created',
110136
cell: ({ value }) => (
111-
<Text>Created {moment(value).format('Do MMM YYYY')}</Text>
137+
<Text>{moment(value).format('HH:mm Do MMM YYYY')}</Text>
112138
),
113139
gridWidth: '1fr',
114140
},
115141
{
142+
header: 'Role',
143+
accessor: 'role',
144+
cell: ({ value }) => (
145+
<Text css={{ textTransform: 'capitalize' }}>{value}</Text>
146+
),
147+
gridWidth: '0.6fr',
148+
},
149+
{
150+
header: 'Copy',
116151
cell: ({ row }) => (
117152
<Button
118153
variant="secondary"
@@ -128,7 +163,8 @@ const Account = ({ token, invites }) => {
128163
<Copy size={24} />
129164
</Button>
130165
),
131-
gridWidth: '32px',
166+
rightAlign: true,
167+
gridWidth: '42px',
132168
},
133169
]}
134170
mb={5}
@@ -164,8 +200,11 @@ export const getServerSideProps = async ({ req }) => {
164200

165201
const {
166202
publicRuntimeConfig: { SQ_API_URL },
203+
serverRuntimeConfig: { SQ_JWT_SECRET },
167204
} = getConfig()
168205

206+
const { role } = jwt.verify(token, SQ_JWT_SECRET)
207+
169208
try {
170209
const invitesRes = await fetch(`${SQ_API_URL}/account/invites`, {
171210
headers: {
@@ -174,7 +213,7 @@ export const getServerSideProps = async ({ req }) => {
174213
},
175214
})
176215
const invites = await invitesRes.json()
177-
return { props: { invites } }
216+
return { props: { invites, userRole: role } }
178217
} catch (e) {
179218
return { props: {} }
180219
}

client/pages/register.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const getServerSideProps = async ({ query: { token } }) => {
9090
const {
9191
serverRuntimeConfig: { SQ_JWT_SECRET, SQ_ALLOW_REGISTER },
9292
} = getConfig()
93-
if (SQ_ALLOW_REGISTER === 'open') return { props: {} }
93+
9494
if (!token && SQ_ALLOW_REGISTER === 'invite')
9595
return { props: { tokenError: 'Invite token not provided' } }
9696
try {

0 commit comments

Comments
 (0)