/
sessions.js
125 lines (110 loc) · 3.35 KB
/
sessions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import bcrypt from 'bcrypt'
import thenify from 'thenify'
import httpErrors from 'http-errors'
import createThrottle from '../throttle/create-throttle'
import throttleMiddleware from '../throttle/middleware'
import redis from '../redis'
import { isUserBanned } from '../models/bans'
import users from '../models/users'
import { getPermissions } from '../models/permissions'
import initSession from '../session/init'
import setReturningCookie from '../session/set-returning-cookie'
// TODO(tec27): Think about maybe a different mechanism for this. I could see this causing problems
// when lots of people need to create sessions at once from the same place (e.g. LAN events)
const loginThrottle = createThrottle('login', {
rate: 4,
burst: 20,
window: 60000,
})
export default function(router) {
router
.get('/', getCurrentSession)
.delete('/', endSession)
.post('/', throttleMiddleware(loginThrottle, ctx => ctx.ip), startNewSession)
}
async function getCurrentSession(ctx, next) {
if (!ctx.session.userId) throw new httpErrors.Gone('Session expired')
const userId = ctx.session.userId
let user
try {
user = await users.find(userId)
} catch (err) {
ctx.log.error({ err }, 'error finding user')
throw err
}
if (!user) {
await ctx.regenerateSession()
throw new httpErrors.Gone('Session expired')
}
ctx.body = { user, permissions: ctx.session.permissions }
}
const bcryptCompare = thenify(bcrypt.compare)
async function startNewSession(ctx, next) {
if (ctx.session.userId) {
const { userId, userName, email, permissions, emailVerified } = ctx.session
ctx.body = {
user: { id: userId, name: userName, email },
permissions,
emailVerified,
}
return
}
const { username, password, remember } = ctx.request.body
if (!username || !password) {
throw new httpErrors.BadRequest('Username and password required')
}
let user
try {
user = await users.find(username)
} catch (err) {
ctx.log.error({ err }, 'error finding user')
throw err
}
if (!user) {
throw new httpErrors.Unauthorized('Incorrect username or password')
}
let same
try {
same = await bcryptCompare(password, user.password)
} catch (err) {
ctx.log.error({ err }, 'error comparing passwords')
throw err
}
if (!same) {
throw new httpErrors.Unauthorized('Incorrect username or password')
}
let isBanned = false
try {
isBanned = await isUserBanned(user.id)
} catch (err) {
ctx.log.error({ err }, 'error checking if user is banned')
throw err
}
if (isBanned) {
throw new httpErrors.Unauthorized('This account has been banned')
}
try {
await ctx.regenerateSession()
const perms = await getPermissions(user.id)
await users.maybeUpdateIp(user.id, ctx.ip)
initSession(ctx, user, perms)
if (!remember) {
// Make the cookie a session-expiring cookie
ctx.session.cookie.maxAge = undefined
ctx.session.cookie.expires = undefined
}
setReturningCookie(ctx)
ctx.body = { user, permissions: perms }
} catch (err) {
ctx.log.error({ err }, 'error regenerating session')
throw err
}
}
async function endSession(ctx, next) {
if (!ctx.session.userId) {
throw new httpErrors.Conflict('No session active')
}
await redis.srem('user_sessions:' + ctx.session.userId, ctx.sessionId)
await ctx.regenerateSession()
ctx.status = 204
}