Added local users
This commit is contained in:
parent
a2889169c3
commit
75de194f88
30
migrations/002-local-auth.sql
Normal file
30
migrations/002-local-auth.sql
Normal file
@ -0,0 +1,30 @@
|
||||
-- Up
|
||||
|
||||
CREATE TABLE User (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
email TEXT,
|
||||
image TEXT,
|
||||
created TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE OAuth (
|
||||
id INTEGER PRIMARY KEY,
|
||||
userId INTEGER,
|
||||
remoteId TEXT,
|
||||
created TEXT,
|
||||
CONSTRAINT PE_fk_userId FOREIGN KEY (userId)
|
||||
REFERENCES User (id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
ALTER TABLE Track ADD COLUMN userId INTEGER;
|
||||
ALTER TABLE Playlist ADD COLUMN userId INTEGER;
|
||||
ALTER TABLE PlaylistEntry ADD COLUMN userId INTEGER;
|
||||
|
||||
-- Down
|
||||
DROP TABLE User;
|
||||
DROP TABLE OAuth;
|
||||
ALTER TABLE Track DROP COLUMN userId;
|
||||
ALTER TABLE Playlist DROP COLUMN userId;
|
||||
ALTER TABLE PlaylistEntry DROP COLUMN userId;
|
@ -12,11 +12,15 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"bcrypt": "^3.0.6",
|
||||
"bluebird": "^3.5.2",
|
||||
"connect-redis": "^3.4.1",
|
||||
"express": "^4.16.3",
|
||||
"express-async-errors": "^3.0.0",
|
||||
"express-session": "^1.16.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"oauth-libre": "^0.9.17",
|
||||
"socket.io": "^2.1.1",
|
||||
"sqlite": "^3.0.2",
|
||||
"sqlite3": "^4.0.6"
|
||||
|
@ -112,6 +112,11 @@
|
||||
<option value="desc">Descending order</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="separator">User</div>
|
||||
<div class="option">
|
||||
<label id="logged-in"></label>
|
||||
<a href="user/logout">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@
|
||||
var playing = document.getElementById('playing')
|
||||
var optdrop = document.getElementById('options-drop')
|
||||
var optmenu = document.getElementById('options')
|
||||
var loggedin = document.getElementById('logged-in')
|
||||
|
||||
var menu = document.getElementById('menu')
|
||||
|
||||
@ -48,6 +49,9 @@
|
||||
sortdir: 'asc'
|
||||
}
|
||||
|
||||
// User info
|
||||
var user = {}
|
||||
|
||||
window.mobilecheck = function() {
|
||||
var check = false;
|
||||
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
|
||||
@ -114,6 +118,7 @@
|
||||
reject(new Error(xmlHttp.status))
|
||||
}
|
||||
}
|
||||
xmlHttp.withCredentials = true
|
||||
xmlHttp.open('GET', url, true)
|
||||
xmlHttp.send(null)
|
||||
})
|
||||
@ -583,6 +588,16 @@
|
||||
}
|
||||
})
|
||||
|
||||
function checkUser () {
|
||||
httpGet('/user/info').then(function (data) {
|
||||
user = data
|
||||
loggedin.innerHTML = 'Logged in as ' + data.username
|
||||
}, function (e) {
|
||||
window.location.href = '/user/login'
|
||||
})
|
||||
}
|
||||
|
||||
checkUser()
|
||||
loadOptions()
|
||||
showTracks(1)
|
||||
handleHash(window.location.hash)
|
||||
|
@ -2,9 +2,12 @@ import path from 'path'
|
||||
import sqlite from 'sqlite'
|
||||
import Promise from 'bluebird'
|
||||
import express from 'express'
|
||||
import session from 'express-session'
|
||||
import redis from 'connect-redis'
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
import { user, userMiddleware } from './user'
|
||||
|
||||
import lfmda from './lastfm'
|
||||
|
||||
@ -35,7 +38,20 @@ const router = express.Router()
|
||||
const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year', 'file']
|
||||
const srchcategories = ['title', 'artist', 'album']
|
||||
|
||||
router.get('/tracks', async (req, res) => {
|
||||
let SessionStore = redis(session)
|
||||
app.use(session({
|
||||
key: values.session_key || 'Session',
|
||||
secret: values.session_secret || 'ch4ng3 m3!',
|
||||
store: new SessionStore(values.redis || { port: 6379 }),
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV !== 'development',
|
||||
maxAge: 2678400000 // 1 month
|
||||
}
|
||||
}))
|
||||
|
||||
router.get('/tracks', userMiddleware, async (req, res) => {
|
||||
let page = parseInt(req.query.page) || 1
|
||||
if (isNaN(page)) {
|
||||
page = 1
|
||||
@ -70,7 +86,7 @@ router.get('/tracks', async (req, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/tracks/search', async (req, res) => {
|
||||
router.get('/tracks/search', userMiddleware, async (req, res) => {
|
||||
let query = req.query.q
|
||||
let streamable = (req.query.streamable === '1')
|
||||
let qr = ''
|
||||
@ -150,7 +166,7 @@ router.get('/tracks/search', async (req, res) => {
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/track/:id', async (req, res, next) => {
|
||||
router.get('/track/:id', userMiddleware, async (req, res, next) => {
|
||||
let id = req.params.id
|
||||
|
||||
let db = await dbPromise
|
||||
@ -165,14 +181,14 @@ router.get('/track/:id', async (req, res, next) => {
|
||||
res.jsonp(track)
|
||||
})
|
||||
|
||||
router.get('/playlists', async (req, res, next) => {
|
||||
router.get('/playlists', userMiddleware, async (req, res, next) => {
|
||||
let db = await dbPromise
|
||||
let playlists = await db.all('SELECT * FROM Playlist')
|
||||
|
||||
res.jsonp(playlists)
|
||||
})
|
||||
|
||||
router.get('/playlist/:id', async (req, res, next) => {
|
||||
router.get('/playlist/:id', userMiddleware, async (req, res, next) => {
|
||||
let id = req.params.id
|
||||
let db = await dbPromise
|
||||
let playlist = await db.get('SELECT title FROM Playlist WHERE id = ?', id)
|
||||
@ -185,7 +201,7 @@ router.get('/playlist/:id', async (req, res, next) => {
|
||||
res.jsonp(playlist)
|
||||
})
|
||||
|
||||
router.get('/serve/by-id/:id', async (req, res, next) => {
|
||||
router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
|
||||
let id = req.params.id
|
||||
let dl = (req.query.dl === '1')
|
||||
let db = await dbPromise
|
||||
@ -217,6 +233,8 @@ router.use((err, req, res, next) => {
|
||||
res.status(404).jsonp({error: 404})
|
||||
})
|
||||
|
||||
app.use('/user', user(dbPromise, values.oauth))
|
||||
|
||||
app.use('/api', router)
|
||||
app.use('/file/track', express.static(path.resolve(values.directory)))
|
||||
app.use('/', express.static(path.join(process.cwd(), 'public')))
|
||||
|
152
src/user.js
Normal file
152
src/user.js
Normal file
@ -0,0 +1,152 @@
|
||||
import express from 'express'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { PromiseOAuth2 } from 'oauth-libre'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
async function userInfoPublic (db, id) {
|
||||
let u = await db.get('SELECT id, username, image FROM User WHERE id = ?', id)
|
||||
if (!u) return {}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
image: u.image
|
||||
}
|
||||
}
|
||||
|
||||
export async function userInfo (db, id) {
|
||||
return db.get('SELECT * FROM User WHERE id = ?', id)
|
||||
}
|
||||
|
||||
export function userMiddleware (req, res, next) {
|
||||
if (!req.session || !req.session.user) return res.status(401).jsonp({ error: 'Unauthorized' })
|
||||
next()
|
||||
}
|
||||
|
||||
export function user (dbPromise, oauth) {
|
||||
router.get('/info', userMiddleware, async (req, res) => {
|
||||
res.jsonp(await userInfoPublic(await dbPromise, req.session.user))
|
||||
})
|
||||
|
||||
router.get('/info/:id', userMiddleware, async (req, res) => {
|
||||
if (isNaN(parseInt(req.params.id))) throw new Error('Invalid user ID!')
|
||||
res.jsonp(await userInfoPublic(await dbPromise, parseInt(req.params.id)))
|
||||
})
|
||||
|
||||
if (!oauth) return router
|
||||
|
||||
let oauth2 = new PromiseOAuth2(oauth.clientId, oauth.clientSecret, oauth.baseUrl, oauth.authorizePath, oauth.tokenPath)
|
||||
|
||||
router.get('/login/oauth/_redirect', async (req, res) => {
|
||||
let code = req.query.code
|
||||
let state = req.query.state
|
||||
if (!code || !state) throw new Error('Something went wrong!')
|
||||
if (!req.session.oauthState || req.session.oauthState !== state) throw new Error('Possible request forgery detected! Try again.')
|
||||
|
||||
delete req.session.oauthState
|
||||
|
||||
let tokens
|
||||
try {
|
||||
tokens = await oauth2.getOAuthAccessToken(code, { grant_type: 'authorization_code', redirect_uri: oauth.redirectUri })
|
||||
} catch (e) {
|
||||
throw new Error('No authorization!')
|
||||
}
|
||||
|
||||
let accessToken = tokens[2].access_token
|
||||
|
||||
// Get user information on remote
|
||||
let userInfo
|
||||
try {
|
||||
userInfo = await oauth2.get(oauth.baseUrl + oauth.userPath, accessToken)
|
||||
userInfo = JSON.parse(userInfo)
|
||||
} catch (e) {
|
||||
userInfo = null
|
||||
}
|
||||
|
||||
if (!userInfo) throw new Error('Couldn\'t get user information!')
|
||||
|
||||
// Let's see if there's a link for this user already..
|
||||
let db = await dbPromise
|
||||
let userLocal = await db.get('SELECT * FROM OAuth WHERE remoteId = ?', userInfo.id)
|
||||
|
||||
// User and link both exist
|
||||
if (userLocal) {
|
||||
if (req.session.user) throw new Error('You are already logged in!')
|
||||
|
||||
req.session.user = userLocal.userId
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
// If we're logged in, create a link
|
||||
if (req.session.user) {
|
||||
await db.run('INSERT INTO OAuth (userId,remoteId,created) VALUES (?,?,?)', req.session.user, userInfo.id, new Date())
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
// Create a new user and log in
|
||||
let newU = await db.get('INSERT INTO User (username,email,image,created) VALUES (?,?,?,?)', userInfo.username, userInfo.email, userInfo.image, new Date())
|
||||
await db.run('INSERT INTO OAuth (userId,remoteId,created) VALUES (?,?,?)', newU.id, userInfo.id, new Date())
|
||||
req.session.user = newU.id
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
router.get('/login/oauth', async (req, res) => {
|
||||
let state = req.session.oauthState || crypto.randomBytes(10).toString('hex')
|
||||
req.session.oauthState = state
|
||||
return res.redirect(oauth2.getAuthorizeUrl({
|
||||
'redirect_uri': oauth.redirectUri,
|
||||
'scope': oauth.scope,
|
||||
'response_type': 'code',
|
||||
'state': state
|
||||
}))
|
||||
})
|
||||
|
||||
router.use('/login', async (req, res, next) => {
|
||||
if (req.session && req.session.user) return res.redirect('/')
|
||||
let header = req.get('authorization') || ''
|
||||
let token = header.split(/\s+/).pop() || ''
|
||||
let auth = Buffer.from(token, 'base64').toString()
|
||||
let parts = auth.split(/:/)
|
||||
let username = parts[0]
|
||||
let password = parts[1]
|
||||
|
||||
let message = oauth != null ? 'Enter \'oauth\' to log in remotely.' : 'Log in'
|
||||
req.message = message
|
||||
|
||||
if ((!username || !password) && (username !== 'oauth' && oauth)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
if ((username === 'oauth' || username === 'oa') && oauth) {
|
||||
return res.redirect('/user/login/oauth')
|
||||
}
|
||||
|
||||
let db = await dbPromise
|
||||
let user = await db.get('SELECT * FROM User WHERE username = ?', username)
|
||||
if (!user) return next()
|
||||
|
||||
if (!user.password && oauth) {
|
||||
return res.redirect('/user/login/oauth')
|
||||
}
|
||||
|
||||
// Compare passwords
|
||||
let ures = await bcrypt.compare(password, user.password)
|
||||
if (!ures) return next()
|
||||
|
||||
// Set login success
|
||||
req.message = ''
|
||||
req.session.user = user.id
|
||||
res.redirect('/')
|
||||
}, function (req, res, next) {
|
||||
if (!req.message) return next()
|
||||
res.status(401).set('WWW-Authenticate', 'Basic realm="' + req.message + '", charset="UTF-8"').end()
|
||||
})
|
||||
|
||||
router.use('/logout', function (req, res) {
|
||||
delete req.session.user
|
||||
res.redirect('/')
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
Loading…
Reference in New Issue
Block a user