Added local users

This commit is contained in:
Evert Prants 2019-06-16 12:28:46 +03:00
parent a2889169c3
commit 75de194f88
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
6 changed files with 230 additions and 6 deletions

View 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;

View File

@ -12,11 +12,15 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"bcrypt": "^3.0.6",
"bluebird": "^3.5.2", "bluebird": "^3.5.2",
"connect-redis": "^3.4.1",
"express": "^4.16.3", "express": "^4.16.3",
"express-async-errors": "^3.0.0", "express-async-errors": "^3.0.0",
"express-session": "^1.16.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.0", "fs-extra": "^7.0.0",
"oauth-libre": "^0.9.17",
"socket.io": "^2.1.1", "socket.io": "^2.1.1",
"sqlite": "^3.0.2", "sqlite": "^3.0.2",
"sqlite3": "^4.0.6" "sqlite3": "^4.0.6"

View File

@ -112,6 +112,11 @@
<option value="desc">Descending order</option> <option value="desc">Descending order</option>
</select> </select>
</div> </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> </div>
</div> </div>

View File

@ -6,6 +6,7 @@
var playing = document.getElementById('playing') var playing = document.getElementById('playing')
var optdrop = document.getElementById('options-drop') var optdrop = document.getElementById('options-drop')
var optmenu = document.getElementById('options') var optmenu = document.getElementById('options')
var loggedin = document.getElementById('logged-in')
var menu = document.getElementById('menu') var menu = document.getElementById('menu')
@ -48,6 +49,9 @@
sortdir: 'asc' sortdir: 'asc'
} }
// User info
var user = {}
window.mobilecheck = function() { window.mobilecheck = function() {
var check = false; 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); (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)) reject(new Error(xmlHttp.status))
} }
} }
xmlHttp.withCredentials = true
xmlHttp.open('GET', url, true) xmlHttp.open('GET', url, true)
xmlHttp.send(null) 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() loadOptions()
showTracks(1) showTracks(1)
handleHash(window.location.hash) handleHash(window.location.hash)

View File

@ -2,9 +2,12 @@ import path from 'path'
import sqlite from 'sqlite' import sqlite from 'sqlite'
import Promise from 'bluebird' import Promise from 'bluebird'
import express from 'express' import express from 'express'
import session from 'express-session'
import redis from 'connect-redis'
import http from 'http' import http from 'http'
import https from 'https' import https from 'https'
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import { user, userMiddleware } from './user'
import lfmda from './lastfm' import lfmda from './lastfm'
@ -35,7 +38,20 @@ const router = express.Router()
const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year', 'file'] const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year', 'file']
const srchcategories = ['title', 'artist', 'album'] 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 let page = parseInt(req.query.page) || 1
if (isNaN(page)) { if (isNaN(page)) {
page = 1 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 query = req.query.q
let streamable = (req.query.streamable === '1') let streamable = (req.query.streamable === '1')
let qr = '' 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 id = req.params.id
let db = await dbPromise let db = await dbPromise
@ -165,14 +181,14 @@ router.get('/track/:id', async (req, res, next) => {
res.jsonp(track) res.jsonp(track)
}) })
router.get('/playlists', async (req, res, next) => { router.get('/playlists', userMiddleware, async (req, res, next) => {
let db = await dbPromise let db = await dbPromise
let playlists = await db.all('SELECT * FROM Playlist') let playlists = await db.all('SELECT * FROM Playlist')
res.jsonp(playlists) 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 id = req.params.id
let db = await dbPromise let db = await dbPromise
let playlist = await db.get('SELECT title FROM Playlist WHERE id = ?', id) 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) 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 id = req.params.id
let dl = (req.query.dl === '1') let dl = (req.query.dl === '1')
let db = await dbPromise let db = await dbPromise
@ -217,6 +233,8 @@ router.use((err, req, res, next) => {
res.status(404).jsonp({error: 404}) res.status(404).jsonp({error: 404})
}) })
app.use('/user', user(dbPromise, values.oauth))
app.use('/api', router) app.use('/api', router)
app.use('/file/track', express.static(path.resolve(values.directory))) app.use('/file/track', express.static(path.resolve(values.directory)))
app.use('/', express.static(path.join(process.cwd(), 'public'))) app.use('/', express.static(path.join(process.cwd(), 'public')))

152
src/user.js Normal file
View 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
}