Skip to content

Commit

Permalink
Merge branch 'room-passwords'
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/routes/Account/components/Login/LoginForm/LoginForm.js
  • Loading branch information
bhj committed Jun 12, 2020
2 parents 8ed686c + 71e2245 commit c2f0be1
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 105 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### New:

- Added [ReplayGain](https://en.wikipedia.org/wiki/ReplayGain) support. With properly tagged media files, the player can automatically adjust each song's volume for a much smoother experience when songs vary widely in average loudness. *Requires re-scanning media*.
- Rooms can now be password-protected
- Added the ability to resize CD+Graphics
- Added Changelog viewer to the About panel

Expand Down
7 changes: 4 additions & 3 deletions server/Queue/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ const {
// ------------------------------------
const ACTION_HANDLERS = {
[QUEUE_ADD]: async (sock, { payload }, acknowledge) => {
// is room open?
if (!await Rooms.isRoomOpen(sock.user.roomId)) {
try {
await Rooms.validate(sock.user.roomId, null, { validatePassword: false })
} catch (err) {
return acknowledge({
type: QUEUE_ADD + '_ERROR',
error: 'Sorry, the room is no longer open',
error: err.message,
})
}

Expand Down
27 changes: 21 additions & 6 deletions server/Rooms/Rooms.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const bcrypt = require('../lib/bcrypt')
const db = require('sqlite')
const sql = require('sqlate')

Expand All @@ -21,28 +22,42 @@ class Rooms {
const res = await db.all(String(query), query.parameters)

res.forEach(row => {
result.push(row.roomId)
row.dateCreated = row.dateCreated.substring(0, 10)
row.hasPassword = !!row.password
delete row.password

result.push(row.roomId)
entities[row.roomId] = row
})

return { result, entities }
}

/**
* Check if a given roomId is open
* Validate a room against optional criteria
*
* @param {Number} roomId
* @return {Promise}
* @param {Number} roomId
* @param {[String]} password Room password
* @param {[Object]} opts (bool) isOpen, (bool) validatePassword
* @return {Promise} True if validated, otherwise throws an error
*/
static async isRoomOpen (roomId) {
static async validate (roomId, password, { isOpen = true, validatePassword = true } = {}) {
const query = sql`
SELECT * FROM rooms
WHERE roomId = ${roomId}
`
const room = await db.get(String(query), query.parameters)
if (!room) throw new Error('Room not found')

if (isOpen && room.status !== 'open') {
throw new Error('Room is no longer open')
}

if (validatePassword && room.password && !await bcrypt.compare(password, room.password)) {
throw new Error('Room password is incorrect')
}

return (room && room.status === 'open')
return true
}

static prefix (roomId = '') {
Expand Down
36 changes: 24 additions & 12 deletions server/Rooms/router.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
const bcrypt = require('../lib/bcrypt')
const db = require('sqlite')
const sql = require('sqlate')
const KoaRouter = require('koa-router')
const router = KoaRouter({ prefix: '/api' })
const log = require('../lib/logger')('Rooms')
const Rooms = require('../Rooms')

const BCRYPT_ROUNDS = 12
const NAME_MIN_LENGTH = 1
const NAME_MAX_LENGTH = 50
const PASSWORD_MIN_LENGTH = 5
const STATUSES = ['open', 'closed']

// list rooms
Expand All @@ -27,21 +31,23 @@ router.post('/rooms', async (ctx, next) => {
ctx.throw(401)
}

let { name, status } = ctx.request.body

name = name.trim()
status = status.trim()
const { name, password, status } = ctx.request.body

if (name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
}

if (password && password.length < PASSWORD_MIN_LENGTH) {
ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
}

if (!status || !STATUSES.includes(status)) {
ctx.throw(400, 'Invalid room status')
}

const fields = new Map()
fields.set('name', name)
fields.set('name', name.trim())
fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
fields.set('status', status)
fields.set('dateCreated', Math.floor(Date.now() / 1000))

Expand All @@ -65,25 +71,31 @@ router.put('/rooms/:roomId', async (ctx, next) => {
ctx.throw(401)
}

let { name, status } = ctx.request.body
const { name, password, status } = ctx.request.body
const roomId = parseInt(ctx.params.roomId, 10)

name = name.trim()
status = status.trim()

if (name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
}

if (password && password.length < PASSWORD_MIN_LENGTH) {
ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
}

if (!status || !STATUSES.includes(status)) {
ctx.throw(400, 'Invalid room status')
}

const fields = new Map()
fields.set('name', name)
fields.set('name', name.trim())
fields.set('status', status)
fields.set('roomId', roomId)

// falsey value will unset password
if (typeof password !== 'undefined') {
fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
}

const query = sql`
UPDATE rooms
SET ${sql.tuple(Array.from(fields.keys()).map(sql.column))} = ${sql.tuple(Array.from(fields.values()))}
Expand Down
58 changes: 27 additions & 31 deletions server/User/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,20 @@ router.put('/account', async (ctx, next) => {

// create account
router.post('/account', async (ctx, next) => {
const creds = await _create(ctx, false) // non-admin
// validate room info first
const { roomId, roomPassword } = ctx.request.body

try {
await Rooms.validate(roomId, roomPassword)
} catch (err) {
ctx.throw(401, err.message)
}

// @todo validate room
const { roomId } = ctx.request.body
// create user
const creds = await _create(ctx, false) // non-admin

// log them in automatically
await _login(ctx, { ...creds, roomId })
await _login(ctx, { ...creds, roomId, roomPassword })
})

// first-time setup
Expand Down Expand Up @@ -288,45 +295,30 @@ async function _create (ctx, isAdmin = 0) {
}

async function _login (ctx, creds) {
const { username, password, roomId } = creds
const { username, password, roomPassword } = creds
const roomId = parseInt(creds.roomId, 10) || undefined

if (!username || !password) {
ctx.throw(422, 'Username/email and password are required')
}

const user = await User.getByUsername(username, true)

if (!user) {
ctx.throw(401)
}

// validate password
if (!await bcrypt.compare(password, user.password)) {
ctx.throw(401)
if (!user || !await bcrypt.compare(password, user.password)) {
ctx.throw(401, 'Incorrect username/email or password')
}

// don't want these in the response
delete user.password
delete user.image

// roomId is required if not an admin
if (!roomId && !user.isAdmin) {
ctx.throw(422, 'Please select a room')
}

// validate roomId
if (roomId) {
const query = sql`
SELECT *
FROM rooms
WHERE roomId = ${roomId}
`
const row = await db.get(String(query), query.parameters)
if (!user.isAdmin) {
if (!roomId) ctx.throw(422, 'Please select a room')

if (!row) ctx.throw(401, 'Invalid roomId')
if (row.status !== 'open') ctx.throw(401, 'Sorry, this room is no longer open')
try {
await Rooms.validate(roomId, roomPassword)
} catch (err) {
ctx.throw(401, err.message)
}

user.roomId = row.roomId
user.roomId = roomId
}

// encrypt JWT based on subset of user object
Expand All @@ -342,5 +334,9 @@ async function _login (ctx, creds) {
httpOnly: true,
})

// don't want these in the response
delete user.password
delete user.image

ctx.body = user
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
margin: 0 0 0 var(--input-margin);
}

.roomId {
.roomSelect {
height: 36px;
}
19 changes: 17 additions & 2 deletions src/routes/Account/components/Account/AccountForm/AccountForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export default class AccountForm extends Component {
}

{this.props.showRoom &&
<RoomSelect onRef={this.handleRoomSelectRef} styleName='field roomId' />
<RoomSelect
onSelectRef={this.handleRoomSelectRef}
onPasswordRef={this.handleRoomPasswordRef}
styleName='field roomSelect'
/>
}

{(this.state.isDirty || !isUser) &&
Expand All @@ -100,7 +104,7 @@ export default class AccountForm extends Component {

if (this.curPassword) {
if (!this.curPassword.value) {
alert('Please enter your current password to make changes.')
alert('Please enter your current password to make changes')
this.curPassword.focus()
return
}
Expand All @@ -126,7 +130,17 @@ export default class AccountForm extends Component {
}

if (this.roomSelect) {
if (!this.roomSelect.value) {
alert('Please select a room')
this.roomSelect.focus()
return
}

data.append('roomId', this.roomSelect.value)

if (this.roomPassword) {
data.append('roomPassword', this.roomPassword.value)
}
}

this.props.onSubmitClick(data)
Expand All @@ -140,6 +154,7 @@ export default class AccountForm extends Component {
}

handleRoomSelectRef = r => { this.roomSelect = r }
handleRoomPasswordRef = r => { this.roomPassword = r }

updateVisible = () => {
if (this.props.user.userId === null) return
Expand Down
52 changes: 42 additions & 10 deletions src/routes/Account/components/Account/RoomSelect/RoomSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import React, { Component } from 'react'
export default class RoomSelect extends Component {
static propTypes = {
rooms: PropTypes.object.isRequired,
onRef: PropTypes.func.isRequired,
onSelectRef: PropTypes.func.isRequired,
onPasswordRef: PropTypes.func.isRequired,
className: PropTypes.string,
// actions
fetchRooms: PropTypes.func.isRequired,
}

state = {
hasPassword: false,
}

componentDidMount () {
this.props.fetchRooms()
}
Expand All @@ -23,26 +28,53 @@ export default class RoomSelect extends Component {
}
}

handleRef = r => {
handleSelectChange = e => {
const roomId = e.target.value
const hasPassword = this.props.rooms.entities[roomId].hasPassword

this.setState({ hasPassword }, () => {
if (this.state.hasPassword) this.input.focus()
})
}

handleInputRef = r => {
this.input = r
this.props.onPasswordRef(r)
}

handleSelectRef = r => {
this.select = r
this.props.onRef(r)
this.props.onSelectRef(r)
}

render () {
const roomOpts = this.props.rooms.result.map(roomId => {
const room = this.props.rooms.entities[roomId]

return (
<option key={roomId} value={roomId}>{room.name}</option>
)
return <option key={roomId} value={roomId}>{room.name}</option>
})

roomOpts.unshift(<option key='choose' value='' disabled>select room...</option>)

return (
<select ref={this.handleRef} defaultValue='' className={this.props.className}>
{roomOpts}
</select>
<>
<select
className={this.props.className}
defaultValue=''
onChange={this.handleSelectChange}
ref={this.handleSelectRef}
>
{roomOpts}
</select>

{this.state.hasPassword &&
<input type='password'
autoComplete='off'
className={this.props.className}
placeholder='room password (required)'
ref={this.handleInputRef}
/>
}
</>
)
}
}
Loading

0 comments on commit c2f0be1

Please sign in to comment.