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,
"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"

View File

@ -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>

View File

@ -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)

View File

@ -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
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
}