Moved away from Wt::Auth and use a simplified (?) login/password system with a login throttler based on IP address
This commit is contained in:
@@ -6,16 +6,16 @@
|
||||
<h2>${title}</h2>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-last-login-attempt>}
|
||||
${<if-has-last-login>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="{id:last-login-attempt}">
|
||||
${tr:Lms.Admin.User.last-login-attempt}
|
||||
<label class="control-label col-sm-2" for="{id:last-login}">
|
||||
${tr:Lms.Admin.User.last-login}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${last-login-attempt}
|
||||
${last-login}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-last-login-attempt>}
|
||||
${</if-has-last-login>}
|
||||
${<if-has-login>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:login}">
|
||||
|
||||
+3
-3
@@ -9,14 +9,14 @@
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2" for="${id:user-name}">
|
||||
<label class="control-label col-sm-2" for="${id:login-name}">
|
||||
${tr:Lms.login}
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
${user-name}
|
||||
${login-name}
|
||||
</div>
|
||||
<div class="help-block col-sm-5">
|
||||
${user-name-info}
|
||||
${login-name-info}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<message id="Lms.logout">Logout</message>
|
||||
<message id="Lms.not-a-directory">Not a directory</message>
|
||||
<message id="Lms.password">Password</message>
|
||||
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
||||
<message id="Lms.password-client-throttled">Login throttled, please try again later</message>
|
||||
<message id="Lms.password-confirm">Confirm password</message>
|
||||
<message id="Lms.password-too-weak">Password too weak</message>
|
||||
<message id="Lms.passwords-dont-match">Passwords don't match</message>
|
||||
<message id="Lms.quit-confirm">Are you sure?</message>
|
||||
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
|
||||
@@ -61,7 +64,7 @@
|
||||
<message id="Lms.Admin.User.demo-account">Demo account</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
||||
<message id="Lms.Admin.User.last-login-attempt">Last login attempt</message>
|
||||
<message id="Lms.Admin.User.last-login">Last login</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">User already exists!</message>
|
||||
<message id="Lms.Admin.User.user-create">New user</message>
|
||||
<message id="Lms.Admin.User.user-created">New user created!</message>
|
||||
@@ -144,10 +147,5 @@
|
||||
<message id="Lms.Settings.settings">Settings</message>
|
||||
<message id="Lms.Settings.settings-saved">New settings saved!</message>
|
||||
|
||||
<!--Wt-->
|
||||
<message id="Wt.Auth.valid"></message>
|
||||
<message id="Wt.Auth.user-name-info"></message>
|
||||
<message id="Wt.Auth.password-info"></message>
|
||||
|
||||
</messages>
|
||||
|
||||
|
||||
+5
-15
@@ -13,7 +13,10 @@
|
||||
<message id="Lms.logout">Logout</message>
|
||||
<message id="Lms.not-a-directory">N'est pas un répertoire</message>
|
||||
<message id="Lms.password">Mot de passe</message>
|
||||
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
||||
<message id="Lms.password-client-throttled">Trop de tentatives de connexion, veuillez réessayer plus tard</message>
|
||||
<message id="Lms.password-confirm">Confirmation du mot de passe</message>
|
||||
<message id="Lms.password-too-weak">Mot de passe trop faible</message>
|
||||
<message id="Lms.passwords-dont-match">Les mots de passe ne correspondent pas</message>
|
||||
<message id="Lms.quit-confirm">Êtes-vous sûr ?</message>
|
||||
<message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
|
||||
@@ -61,7 +64,7 @@
|
||||
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
|
||||
<message id="Lms.Admin.User.last-login-attempt">Dernière tentative de login</message>
|
||||
<message id="Lms.Admin.User.last-login">Date du dernier login</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">L'utilisateur existe déjà !</message>
|
||||
<message id="Lms.Admin.User.user-create">Nouvel utilisateur</message>
|
||||
<message id="Lms.Admin.User.user-created">Nouvel utilisateur créé !</message>
|
||||
@@ -90,7 +93,7 @@
|
||||
<message id="Lms.Explore.various-artists">Artistes divers</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Artists similaires</message>
|
||||
<message id="Lms.Explore.ArtistInfo.similar-artists">Artistes similaires</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.ArtistsInfo.recently-added">Ajouts récents</message>
|
||||
@@ -149,17 +152,4 @@
|
||||
<message id="Wt.WMessageBox.Yes">Oui</message>
|
||||
<message id="Wt.WMessageBox.No">Non</message>
|
||||
|
||||
<message id="Wt.Auth.valid"></message>
|
||||
<message id="Wt.Auth.user-name-info"></message>
|
||||
<message id="Wt.Auth.user-name-invalid">Invalide</message>
|
||||
<message id="Wt.Auth.password-info"></message>
|
||||
<message id="Wt.Auth.password-invalid">Mot de passe invalide</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-short">Trop court</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-long">Trop long</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-simpleshort">Pas assez de caractères ou de classes différentes pour cette longueur de mot de passe</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-simple">Pas assez de caractères ou de classes différentes</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-personal">Basé sur des informations personnelles</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-word">Basé sur un mot de dictionnaire</message>
|
||||
<message id="Wt.Auth.passwdqc.reason-seq">Basé sur une séquence de caractères</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -32,3 +32,6 @@ log-config = "* -debug -info:WebRequest";
|
||||
# Turn on this option to allow the demo account creation/use
|
||||
demo = false;
|
||||
|
||||
# Max entries in the logging throttler (1 entry per client)
|
||||
login-throttler-max-entries = 10000;
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ lms_SOURCES = \
|
||||
$(srcdir)/api/subsonic/SubsonicResource.hpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResponse.cpp \
|
||||
$(srcdir)/api/subsonic/SubsonicResponse.hpp \
|
||||
$(srcdir)/auth/AuthService.cpp \
|
||||
$(srcdir)/auth/AuthService.hpp \
|
||||
$(srcdir)/auth/LoginThrottler.cpp \
|
||||
$(srcdir)/auth/LoginThrottler.hpp \
|
||||
$(srcdir)/av/AvInfo.cpp \
|
||||
$(srcdir)/av/AvInfo.hpp \
|
||||
$(srcdir)/av/AvTranscoder.cpp \
|
||||
@@ -134,6 +138,8 @@ lms_SOURCES = \
|
||||
$(srcdir)/utils/Exception.hpp \
|
||||
$(srcdir)/utils/Logger.cpp \
|
||||
$(srcdir)/utils/Logger.hpp \
|
||||
$(srcdir)/utils/NetAddress.cpp \
|
||||
$(srcdir)/utils/NetAddress.hpp \
|
||||
$(srcdir)/utils/Path.cpp \
|
||||
$(srcdir)/utils/Path.hpp \
|
||||
$(srcdir)/utils/Utils.cpp \
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
#include <random>
|
||||
#include <thread>
|
||||
|
||||
#include <Wt/Auth/Identity.h>
|
||||
#include <Wt/WLocalDateTime.h>
|
||||
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
@@ -42,6 +42,8 @@
|
||||
#include "SubsonicId.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
static const std::string genreClusterName {"GENRE"};
|
||||
// Files are always reported to be in the same format
|
||||
static const std::size_t reportedBitrate {128};
|
||||
@@ -115,33 +117,41 @@ struct ClientInfo
|
||||
struct RequestContext
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
Database::Session& dbSession;
|
||||
Session& dbSession;
|
||||
std::string userName;
|
||||
};
|
||||
|
||||
// TODO handle multiple databases
|
||||
static thread_local std::map<std::thread::id, std::unique_ptr<Database::Session>> dbSessions;
|
||||
using SessionMap = std::map<Database::Database*, std::unique_ptr<Session>>;
|
||||
static std::map<std::thread::id, SessionMap> dbSessions;
|
||||
|
||||
static
|
||||
Database::Session&
|
||||
Session&
|
||||
getOrCreateDbSession(Database::Database& db)
|
||||
{
|
||||
static std::mutex mutex;
|
||||
|
||||
std::unique_lock<std::mutex> lock {mutex};
|
||||
SessionMap* sessionMap {};
|
||||
|
||||
auto it {dbSessions.find(std::this_thread::get_id())};
|
||||
if (it != dbSessions.end())
|
||||
{
|
||||
std::unique_lock<std::mutex> lock {mutex};
|
||||
sessionMap = &dbSessions[std::this_thread::get_id()];
|
||||
}
|
||||
|
||||
auto it {sessionMap->find(&db)};
|
||||
if (it != std::end(*sessionMap))
|
||||
return *it->second;
|
||||
|
||||
auto res {dbSessions.emplace(std::this_thread::get_id(), db.createSession())};
|
||||
auto res {sessionMap->emplace(&db, db.createSession())};
|
||||
assert(res.second);
|
||||
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Created db session";
|
||||
|
||||
return *(res.first->second);
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
cleanDbSessions()
|
||||
clearDbSessions()
|
||||
{
|
||||
dbSessions.clear();
|
||||
}
|
||||
@@ -326,7 +336,7 @@ SubsonicResource::SubsonicResource(Database::Database& db)
|
||||
SubsonicResource::~SubsonicResource()
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, DEBUG) << "Cleaning db sessions...";
|
||||
cleanDbSessions();
|
||||
clearDbSessions();
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
@@ -379,7 +389,7 @@ std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap
|
||||
void
|
||||
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
|
||||
{
|
||||
static std::atomic<std::size_t> curRequestId{};
|
||||
static std::atomic<std::size_t> curRequestId {};
|
||||
|
||||
const std::size_t requestId {curRequestId++};
|
||||
|
||||
@@ -392,12 +402,21 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
||||
|
||||
try
|
||||
{
|
||||
Database::Session& dbSession {getOrCreateDbSession(_db)};
|
||||
Session& dbSession {getOrCreateDbSession(_db)};
|
||||
|
||||
const ClientInfo clientInfo {getClientInfo(parameters)};
|
||||
|
||||
if (!dbSession.checkUserPassword(clientInfo.user, clientInfo.password))
|
||||
throw Error {Error::Code::WrongUsernameOrPassword};
|
||||
switch (getService<Auth::AuthService>()->checkUserPassword(dbSession,
|
||||
boost::asio::ip::address::from_string(request.clientAddress()),
|
||||
clientInfo.user, clientInfo.password))
|
||||
{
|
||||
case Auth::AuthService::PasswordCheckResult::Match:
|
||||
break;
|
||||
case Auth::AuthService::PasswordCheckResult::Mismatch:
|
||||
throw Error {Error::Code::WrongUsernameOrPassword};
|
||||
case Auth::AuthService::PasswordCheckResult::Throttled:
|
||||
throw Error {Error::CustomType::LoginThrottled};
|
||||
}
|
||||
|
||||
RequestContext requestContext {.parameters = parameters, .dbSession = dbSession, .userName = clientInfo.user};
|
||||
|
||||
@@ -455,7 +474,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
||||
|
||||
static
|
||||
std::string
|
||||
getArtistNames(const std::vector<Database::Artist::pointer>& artists)
|
||||
getArtistNames(const std::vector<Artist::pointer>& artists)
|
||||
{
|
||||
if (artists.size() == 1)
|
||||
return artists.front()->getName();
|
||||
@@ -464,7 +483,7 @@ getArtistNames(const std::vector<Database::Artist::pointer>& artists)
|
||||
names.resize(artists.size());
|
||||
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::begin(names),
|
||||
[](const Database::Artist::pointer& artist)
|
||||
[](const Artist::pointer& artist)
|
||||
{
|
||||
return artist->getName();
|
||||
});
|
||||
@@ -474,7 +493,7 @@ getArtistNames(const std::vector<Database::Artist::pointer>& artists)
|
||||
|
||||
static
|
||||
std::string
|
||||
getTrackPath(const Database::Track::pointer& track)
|
||||
getTrackPath(const Track::pointer& track)
|
||||
{
|
||||
std::string path;
|
||||
|
||||
@@ -503,7 +522,7 @@ getTrackPath(const Database::Track::pointer& track)
|
||||
|
||||
static
|
||||
Response::Node
|
||||
trackToResponseNode(const Database::Track::pointer& track, Database::Session& dbSession, const Database::User::pointer& user)
|
||||
trackToResponseNode(const Track::pointer& track, Session& dbSession, const User::pointer& user)
|
||||
{
|
||||
Response::Node trackResponse;
|
||||
|
||||
@@ -547,7 +566,7 @@ trackToResponseNode(const Database::Track::pointer& track, Database::Session& db
|
||||
trackResponse.setAttribute("starred", reportedStarredDate);
|
||||
|
||||
// Report the first GENRE for this track
|
||||
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(dbSession, genreClusterName)};
|
||||
ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)};
|
||||
if (clusterType)
|
||||
{
|
||||
auto clusters {track->getClusterGroups({clusterType}, 1)};
|
||||
@@ -560,7 +579,7 @@ trackToResponseNode(const Database::Track::pointer& track, Database::Session& db
|
||||
|
||||
static
|
||||
Response::Node
|
||||
releaseToResponseNode(const Database::Release::pointer& release, Database::Session& dbSession, const Database::User::pointer& user, bool id3)
|
||||
releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3)
|
||||
{
|
||||
Response::Node albumNode;
|
||||
|
||||
@@ -613,7 +632,7 @@ releaseToResponseNode(const Database::Release::pointer& release, Database::Sessi
|
||||
if (id3)
|
||||
{
|
||||
// Report the first GENRE for this track
|
||||
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(dbSession, genreClusterName)};
|
||||
ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)};
|
||||
if (clusterType)
|
||||
{
|
||||
auto clusters {release->getClusterGroups({clusterType}, 1)};
|
||||
@@ -630,7 +649,7 @@ releaseToResponseNode(const Database::Release::pointer& release, Database::Sessi
|
||||
|
||||
static
|
||||
Response::Node
|
||||
artistToResponseNode(const Database::User::pointer& user, const Database::Artist::pointer& artist, bool id3)
|
||||
artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, bool id3)
|
||||
{
|
||||
Response::Node artistNode;
|
||||
|
||||
@@ -648,7 +667,7 @@ artistToResponseNode(const Database::User::pointer& user, const Database::Artist
|
||||
|
||||
static
|
||||
Response::Node
|
||||
clusterToResponseNode(const Database::Cluster::pointer& cluster)
|
||||
clusterToResponseNode(const Cluster::pointer& cluster)
|
||||
{
|
||||
Response::Node clusterNode;
|
||||
|
||||
@@ -683,19 +702,19 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
if (!name && !id)
|
||||
throw Error {Error::Code::RequiredParameterMissing};
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Database::TrackList::pointer tracklist;
|
||||
TrackList::pointer tracklist;
|
||||
if (id)
|
||||
{
|
||||
tracklist = Database::TrackList::getById(context.dbSession, id->value);
|
||||
tracklist = TrackList::getById(context.dbSession, id->value);
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != Database::TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
{
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
}
|
||||
@@ -705,16 +724,16 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
}
|
||||
else
|
||||
{
|
||||
tracklist = Database::TrackList::create(context.dbSession, *name, Database::TrackList::Type::Playlist, false, user);
|
||||
tracklist = TrackList::create(context.dbSession, *name, TrackList::Type::Playlist, false, user);
|
||||
}
|
||||
|
||||
for (const Id& trackId : trackIds)
|
||||
{
|
||||
Database::Track::pointer track {Database::Track::getById(context.dbSession, trackId.value)};
|
||||
Track::pointer track {Track::getById(context.dbSession, trackId.value)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
Database::TrackListEntry::create(context.dbSession, track, tracklist );
|
||||
TrackListEntry::create(context.dbSession, track, tracklist );
|
||||
}
|
||||
|
||||
return Response::createOkResponse();
|
||||
@@ -730,14 +749,14 @@ handleDeletePlaylistRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)};
|
||||
TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)};
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != Database::TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
{
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
}
|
||||
@@ -769,53 +788,21 @@ handleGetRandomSongsRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
auto tracks {Database::Track::getAllRandom(context.dbSession, size)};
|
||||
auto tracks {Track::getAllRandom(context.dbSession, size)};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
|
||||
Response::Node& randomSongsNode {response.createNode("randomSongs")};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
for (const Track::pointer& track : tracks)
|
||||
randomSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Database::Release::pointer> getRandomAlbums(Database::Session& dbSession, std::size_t offset, std::size_t size)
|
||||
{
|
||||
std::vector<Database::Release::pointer> res;
|
||||
|
||||
std::size_t nbReleases {Database::Release::getCount(dbSession)};
|
||||
if (offset > nbReleases)
|
||||
return res;
|
||||
|
||||
if (offset + size > nbReleases)
|
||||
size = nbReleases - offset;
|
||||
|
||||
std::vector<size_t> indexes;
|
||||
indexes.resize(nbReleases);
|
||||
std::iota(std::begin(indexes), std::end(indexes), 1);
|
||||
|
||||
// As random results are paginated, we need to set a seed for it
|
||||
std::seed_seq seed {1337};
|
||||
std::mt19937 generator{seed};
|
||||
|
||||
std::shuffle(std::begin(indexes), std::end(indexes), generator);
|
||||
std::for_each(std::next(std::begin(indexes), offset), std::next(std::begin(indexes), offset + size),
|
||||
[&](std::size_t offset)
|
||||
{
|
||||
auto release {Database::Release::getAll(dbSession, offset, 1)};
|
||||
if (!release.empty())
|
||||
res.emplace_back(release.front());
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
@@ -827,40 +814,41 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").get_value_or(10)};
|
||||
std::size_t offset {getParameterAs<std::size_t>(context.parameters, "offset").get_value_or(0)};
|
||||
|
||||
std::vector<Database::Release::pointer> releases;
|
||||
std::vector<Release::pointer> releases;
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
if (type == "random")
|
||||
{
|
||||
releases = getRandomAlbums(context.dbSession, offset, size);
|
||||
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||
releases = Release::getAllRandom(context.dbSession, size);
|
||||
}
|
||||
else if (type == "newest")
|
||||
{
|
||||
auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)};
|
||||
releases = Database::Release::getLastAdded(context.dbSession, after, offset, size);
|
||||
releases = Release::getLastAdded(context.dbSession, after, offset, size);
|
||||
}
|
||||
else if (type == "alphabeticalByName")
|
||||
{
|
||||
releases = Database::Release::getAll(context.dbSession, offset, size);
|
||||
releases = Release::getAll(context.dbSession, offset, size);
|
||||
}
|
||||
else if (type == "byGenre")
|
||||
{
|
||||
// Mandatory param
|
||||
std::string genre {getMandatoryParameterAs<std::string>(context.parameters, "genre")};
|
||||
|
||||
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
if (clusterType)
|
||||
{
|
||||
Database::Cluster::pointer cluster {clusterType->getCluster(genre)};
|
||||
Cluster::pointer cluster {clusterType->getCluster(genre)};
|
||||
if (cluster)
|
||||
{
|
||||
bool more;
|
||||
releases = Database::Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more);
|
||||
releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -870,7 +858,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")};
|
||||
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
for (const Release::pointer& release : releases)
|
||||
albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
|
||||
return response;
|
||||
@@ -899,11 +887,11 @@ handleGetAlbumRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)};
|
||||
Release::pointer release {Release::getById(context.dbSession, id.value)};
|
||||
if (!release)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -911,7 +899,7 @@ handleGetAlbumRequest(RequestContext& context)
|
||||
Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)};
|
||||
|
||||
auto tracks {release->getTracks()};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
for (const Track::pointer& track : tracks)
|
||||
releaseNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
response.addNode("album", std::move(releaseNode));
|
||||
@@ -930,8 +918,8 @@ handleGetArtistRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
Artist::pointer artist {Artist::getById(context.dbSession, id.value)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
|
||||
if (!artist || !user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
@@ -940,7 +928,7 @@ handleGetArtistRequest(RequestContext& context)
|
||||
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
for (const Release::pointer& release : releases)
|
||||
artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */));
|
||||
|
||||
response.addNode("artist", std::move(artistNode));
|
||||
@@ -960,27 +948,35 @@ Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
||||
// Optional params
|
||||
std::size_t count {getParameterAs<std::size_t>(context.parameters, "count").get_value_or(10)};
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
|
||||
if (!artist || !user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
|
||||
|
||||
if (!artist->getMBID().empty())
|
||||
artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID());
|
||||
|
||||
auto similarArtistsId {getService<Similarity::Searcher>()->getSimilarArtists(context.dbSession, artist.id(), count)};
|
||||
for ( const auto& similarArtistId : similarArtistsId )
|
||||
{
|
||||
Database::Artist::pointer similarArtist {Database::Artist::getById(context.dbSession, similarArtistId)};
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
if (similarArtist)
|
||||
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3));
|
||||
Artist::pointer artist {Artist::getById(context.dbSession, id.value)};
|
||||
if (!artist)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
if (!artist->getMBID().empty())
|
||||
artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID());
|
||||
}
|
||||
|
||||
auto similarArtistsId {getService<Similarity::Searcher>()->getSimilarArtists(context.dbSession, id.value, count)};
|
||||
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
for ( const auto& similarArtistId : similarArtistsId )
|
||||
{
|
||||
Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)};
|
||||
if (similarArtist)
|
||||
artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -1006,12 +1002,12 @@ handleGetArtistsRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
auto artists {Database::Artist::getAll(context.dbSession)};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
||||
|
||||
return response;
|
||||
@@ -1031,7 +1027,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1041,8 +1037,8 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
{
|
||||
directoryNode.setAttribute("name", "Music");
|
||||
|
||||
auto artists {Database::Artist::getAll(context.dbSession)};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
break;
|
||||
@@ -1050,14 +1046,14 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
case Id::Type::Artist:
|
||||
{
|
||||
auto artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
auto artist {Artist::getById(context.dbSession, id.value)};
|
||||
if (!artist)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
|
||||
|
||||
auto releases {artist->getReleases()};
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
for (const Release::pointer& release : releases)
|
||||
directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */));
|
||||
|
||||
break;
|
||||
@@ -1065,14 +1061,14 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
|
||||
case Id::Type::Release:
|
||||
{
|
||||
auto release {Database::Release::getById(context.dbSession, id.value)};
|
||||
auto release {Release::getById(context.dbSession, id.value)};
|
||||
if (!release)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName()));
|
||||
|
||||
auto tracks {release->getTracks()};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
for (const Track::pointer& track : tracks)
|
||||
directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
break;
|
||||
@@ -1107,12 +1103,12 @@ handleGetGenresRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
auto clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
if (clusterType)
|
||||
{
|
||||
auto clusters {clusterType->getClusters()};
|
||||
|
||||
for (const Database::Cluster::pointer& cluster : clusters)
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
genresNode.addArrayChild("genre", clusterToResponseNode(cluster));
|
||||
}
|
||||
|
||||
@@ -1130,12 +1126,12 @@ handleGetIndexesRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
auto artists {Database::Artist::getAll(context.dbSession)};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
return response;
|
||||
@@ -1154,8 +1150,8 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
Artist::pointer artist {Artist::getById(context.dbSession, id.value)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
|
||||
if (!user || !artist)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
@@ -1166,7 +1162,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
auto similarArtistsId {getService<Similarity::Searcher>()->getSimilarArtists(context.dbSession, artist.id(), 5)};
|
||||
for ( const auto& similarArtistId : similarArtistsId )
|
||||
{
|
||||
Database::Artist::pointer similarArtist {Database::Artist::getById(context.dbSession, similarArtistId)};
|
||||
Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)};
|
||||
if (!similarArtist)
|
||||
continue;
|
||||
|
||||
@@ -1183,7 +1179,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
for (const Track::pointer& track : tracks)
|
||||
similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
return response;
|
||||
@@ -1208,7 +1204,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1217,19 +1213,19 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
{
|
||||
auto artists {user->getStarredArtists()};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
for (const Artist::pointer& artist : artists)
|
||||
starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto releases {user->getStarredReleases()};
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
for (const Release::pointer& release : releases)
|
||||
starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto tracks {user->getStarredTracks()};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
for (const Track::pointer& track : tracks)
|
||||
starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
@@ -1250,7 +1246,7 @@ handleGetStarred2Request(RequestContext& context)
|
||||
}
|
||||
|
||||
Response::Node
|
||||
tracklistToResponseNode(const Database::TrackList::pointer& tracklist, Database::Session& dbSession)
|
||||
tracklistToResponseNode(const TrackList::pointer& tracklist, Session& dbSession)
|
||||
{
|
||||
Response::Node playlistNode;
|
||||
|
||||
@@ -1260,7 +1256,7 @@ tracklistToResponseNode(const Database::TrackList::pointer& tracklist, Database:
|
||||
playlistNode.setAttribute("duration", std::to_string(std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count()));
|
||||
playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false");
|
||||
playlistNode.setAttribute("created", "");
|
||||
playlistNode.setAttribute("owner", dbSession.getUserLoginName(tracklist->getUser()));
|
||||
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
||||
|
||||
return playlistNode;
|
||||
}
|
||||
@@ -1275,8 +1271,8 @@ handleGetPlaylistRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)};
|
||||
if (!user || !tracklist)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1284,7 +1280,7 @@ handleGetPlaylistRequest(RequestContext& context)
|
||||
Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)};
|
||||
|
||||
auto entries {tracklist->getEntries()};
|
||||
for (const Database::TrackListEntry::pointer& entry : entries)
|
||||
for (const TrackListEntry::pointer& entry : entries)
|
||||
playlistNode.addArrayChild("entry", trackToResponseNode(entry->getTrack(), context.dbSession, user));
|
||||
|
||||
response.addNode("playlist", playlistNode );
|
||||
@@ -1297,15 +1293,15 @@ handleGetPlaylistsRequest(RequestContext& context)
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Response response {Response::createOkResponse()};
|
||||
Response::Node& playlistsNode {response.createNode("playlists")};
|
||||
|
||||
auto tracklists {Database::TrackList::getAll(context.dbSession, user, Database::TrackList::Type::Playlist)};
|
||||
for (const Database::TrackList::pointer& tracklist : tracklists)
|
||||
auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)};
|
||||
for (const TrackList::pointer& tracklist : tracklists)
|
||||
playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.dbSession));
|
||||
|
||||
return response;
|
||||
@@ -1325,7 +1321,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
auto clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)};
|
||||
if (!clusterType)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1333,7 +1329,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
if (!cluster)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1341,8 +1337,8 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
|
||||
|
||||
bool more;
|
||||
auto tracks {Database::Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
|
||||
return response;
|
||||
@@ -1367,7 +1363,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1376,20 +1372,20 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
bool more;
|
||||
{
|
||||
auto artists {Database::Artist::getByFilter(context.dbSession, {}, keywords, artistOffset, artistCount, more)};
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, artistOffset, artistCount, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto releases {Database::Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)};
|
||||
for (const Database::Release::pointer& release : releases)
|
||||
auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)};
|
||||
for (const Release::pointer& release : releases)
|
||||
searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
{
|
||||
auto tracks {Database::Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)};
|
||||
for (const Database::Track::pointer& track : tracks)
|
||||
auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)};
|
||||
for (const Track::pointer& track : tracks)
|
||||
searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
@@ -1448,13 +1444,13 @@ handleStarRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
for (const Id& id : params.artistIds)
|
||||
{
|
||||
Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
Artist::pointer artist {Artist::getById(context.dbSession, id.value)};
|
||||
if (!artist)
|
||||
continue;
|
||||
|
||||
@@ -1463,7 +1459,7 @@ handleStarRequest(RequestContext& context)
|
||||
|
||||
for (const Id& id : params.releaseIds)
|
||||
{
|
||||
Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)};
|
||||
Release::pointer release {Release::getById(context.dbSession, id.value)};
|
||||
if (!release)
|
||||
continue;
|
||||
|
||||
@@ -1472,7 +1468,7 @@ handleStarRequest(RequestContext& context)
|
||||
|
||||
for (const Id& id : params.trackIds)
|
||||
{
|
||||
Database::Track::pointer track {Database::Track::getById(context.dbSession, id.value)};
|
||||
Track::pointer track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
@@ -1501,13 +1497,13 @@ handleUnstarRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
for (const Id& id : params.artistIds)
|
||||
{
|
||||
Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)};
|
||||
Artist::pointer artist {Artist::getById(context.dbSession, id.value)};
|
||||
if (!artist)
|
||||
continue;
|
||||
|
||||
@@ -1516,7 +1512,7 @@ handleUnstarRequest(RequestContext& context)
|
||||
|
||||
for (const Id& id : params.releaseIds)
|
||||
{
|
||||
Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)};
|
||||
Release::pointer release {Release::getById(context.dbSession, id.value)};
|
||||
if (!release)
|
||||
continue;
|
||||
|
||||
@@ -1525,7 +1521,7 @@ handleUnstarRequest(RequestContext& context)
|
||||
|
||||
for (const Id& id : params.trackIds)
|
||||
{
|
||||
Database::Track::pointer track {Database::Track::getById(context.dbSession, id.value)};
|
||||
Track::pointer track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
@@ -1556,14 +1552,14 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)};
|
||||
TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)};
|
||||
if (!tracklist
|
||||
|| tracklist->getUser() != user
|
||||
|| tracklist->getType() != Database::TrackList::Type::Playlist)
|
||||
|| tracklist->getType() != TrackList::Type::Playlist)
|
||||
{
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
}
|
||||
@@ -1589,11 +1585,11 @@ handleUpdatePlaylistRequest(RequestContext& context)
|
||||
// Add tracks
|
||||
for (const Id& trackIdToAdd : trackIdsToAdd)
|
||||
{
|
||||
Database::Track::pointer track {Database::Track::getById(context.dbSession, trackIdToAdd.value)};
|
||||
Track::pointer track {Track::getById(context.dbSession, trackIdToAdd.value)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
Database::TrackListEntry::create(context.dbSession, track, tracklist );
|
||||
TrackListEntry::create(context.dbSession, track, tracklist );
|
||||
}
|
||||
|
||||
return Response::createOkResponse();
|
||||
@@ -1613,7 +1609,7 @@ createTranscoder(RequestContext& context)
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Database::User::pointer user {context.dbSession.getUser(context.userName)};
|
||||
User::pointer user {User::getByLoginName(context.dbSession, context.userName)};
|
||||
if (!user)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
@@ -1623,7 +1619,7 @@ createTranscoder(RequestContext& context)
|
||||
|
||||
*maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000);
|
||||
|
||||
auto track {Database::Track::getById(context.dbSession, id.value)};
|
||||
auto track {Track::getById(context.dbSession, id.value)};
|
||||
if (!track)
|
||||
throw Error {Error::Code::RequestedDataNotFound};
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ ErrorCodeToString(Error::Code error)
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case Error::Code::Generic:
|
||||
return "Generic Error.";
|
||||
case Error::Code::RequiredParameterMissing:
|
||||
return "Required parameter is missing.";
|
||||
case Error::Code::ClientMustUpgrade:
|
||||
@@ -64,9 +66,9 @@ ErrorCodeToString(Error::Code error)
|
||||
return "User is not authorized for the given operation.";
|
||||
case Error::Code::RequestedDataNotFound:
|
||||
return "The requested data was not found.";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
static
|
||||
@@ -81,9 +83,11 @@ CustomTypeToString(Error::CustomType type)
|
||||
return "Not implemented";
|
||||
case Error::CustomType::InternalError:
|
||||
return "Internal error";
|
||||
default:
|
||||
return "Unknown custom error";
|
||||
case Error::CustomType::LoginThrottled:
|
||||
return "Login throttled, too many attempts";
|
||||
}
|
||||
|
||||
return "Unknown custom error";
|
||||
}
|
||||
|
||||
Error::Error(Code code)
|
||||
|
||||
@@ -50,6 +50,7 @@ class Error
|
||||
{
|
||||
BadId,
|
||||
NotImplemented,
|
||||
LoginThrottled,
|
||||
InternalError,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#include "AuthService.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <Wt/Auth/HashFunction.h>
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
#include <Wt/Utils.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
AuthService::AuthService(std::size_t maxThrottlerEntries)
|
||||
: _loginThrottler {maxThrottlerEntries}
|
||||
{
|
||||
}
|
||||
|
||||
AuthService::PasswordCheckResult
|
||||
AuthService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password)
|
||||
{
|
||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||
return PasswordCheckResult::Throttled;
|
||||
|
||||
Database::User::PasswordHash passwordHash;
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||
return PasswordCheckResult::Mismatch;
|
||||
}
|
||||
|
||||
passwordHash = user->getPasswordHash();
|
||||
}
|
||||
|
||||
const Wt::Auth::BCryptHashFunction hashFunc {6};
|
||||
if (hashFunc.verify(password, passwordHash.salt, passwordHash.hash))
|
||||
{
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
return PasswordCheckResult::Match;
|
||||
}
|
||||
else
|
||||
{
|
||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||
return PasswordCheckResult::Mismatch;
|
||||
}
|
||||
}
|
||||
|
||||
Database::User::PasswordHash
|
||||
AuthService::hashPassword(const std::string& password) const
|
||||
{
|
||||
std::array<std::uint8_t, 16> buffer;
|
||||
fillRandom(buffer);
|
||||
|
||||
std::ostringstream oss;
|
||||
for (std::uint8_t b : buffer)
|
||||
oss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(b);
|
||||
|
||||
const std::string salt {Wt::Utils::base64Encode(oss.str(), false)};
|
||||
|
||||
const Wt::Auth::BCryptHashFunction hashFunc {6};
|
||||
return {salt, hashFunc.compute(password, salt)};
|
||||
}
|
||||
|
||||
bool
|
||||
AuthService::evaluatePasswordStrength(const std::string& loginName, const std::string& password) const
|
||||
{
|
||||
Wt::Auth::PasswordStrengthValidator validator;
|
||||
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
||||
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
||||
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
|
||||
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4);
|
||||
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4);
|
||||
validator.setMinimumPassPhraseWords(1);
|
||||
validator.setMinimumMatchLength(3);
|
||||
|
||||
return validator.evaluateStrength(password, loginName, "").isValid();
|
||||
}
|
||||
|
||||
} // namespace Auth
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public:
|
||||
|
||||
AuthService(std::size_t maxThrottlerEntries);
|
||||
|
||||
AuthService() = default;
|
||||
~AuthService() = default;
|
||||
|
||||
AuthService(const AuthService&) = delete;
|
||||
AuthService& operator=(const AuthService&) = delete;
|
||||
AuthService(AuthService&&) = delete;
|
||||
AuthService& operator=(AuthService&&) = delete;
|
||||
|
||||
|
||||
// Password services
|
||||
enum class PasswordCheckResult
|
||||
{
|
||||
Match,
|
||||
Mismatch,
|
||||
Throttled,
|
||||
};
|
||||
PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password);
|
||||
Database::User::PasswordHash hashPassword(const std::string& password) const;
|
||||
bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const;
|
||||
|
||||
private:
|
||||
|
||||
LoginThrottler _loginThrottler;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
static
|
||||
boost::asio::ip::address_v6
|
||||
getAddressWithMask(const boost::asio::ip::address_v6& address, std::size_t prefix)
|
||||
{
|
||||
assert(prefix % 8 == 0);
|
||||
|
||||
std::array<uint8_t, 16> truncatedBytes;
|
||||
|
||||
auto bytes {address.to_bytes()};
|
||||
std::copy(std::cbegin(bytes), std::next(std::cbegin(bytes), prefix / 8), truncatedBytes.begin());
|
||||
|
||||
return boost::asio::ip::address_v6 {truncatedBytes};
|
||||
}
|
||||
|
||||
static
|
||||
boost::asio::ip::address
|
||||
getAddressToThrottle(const boost::asio::ip::address& address)
|
||||
{
|
||||
return address.is_v6() ? getAddressWithMask(address.to_v6(), 64) : address;
|
||||
}
|
||||
|
||||
void
|
||||
LoginThrottler::removeOutdatedEntries()
|
||||
{
|
||||
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
|
||||
|
||||
for (auto it {std::begin(_attemptsInfo)}; it != std::end(_attemptsInfo); )
|
||||
{
|
||||
if (it->second.nextValidAttempt <= now)
|
||||
it = _attemptsInfo.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
|
||||
{
|
||||
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
|
||||
|
||||
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
|
||||
|
||||
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
|
||||
|
||||
if (_attemptsInfo.size() >= _maxEntries)
|
||||
removeOutdatedEntries();
|
||||
// If still full, kill one random entry
|
||||
if (_attemptsInfo.size() >= _maxEntries)
|
||||
_attemptsInfo.erase(pickRandom(_attemptsInfo));
|
||||
|
||||
AttemptsInfo& attemptsInfo {_attemptsInfo[address]};
|
||||
|
||||
attemptsInfo.nbSuccessiveBadAttempts++;
|
||||
|
||||
if (attemptsInfo.nbSuccessiveBadAttempts >= 50)
|
||||
attemptsInfo.nextValidAttempt = now.addSecs(60);
|
||||
if (attemptsInfo.nbSuccessiveBadAttempts >= 20)
|
||||
attemptsInfo.nextValidAttempt = now.addSecs(10);
|
||||
else if (attemptsInfo.nbSuccessiveBadAttempts >= 10)
|
||||
attemptsInfo.nextValidAttempt = now.addSecs(5);
|
||||
else if (attemptsInfo.nbSuccessiveBadAttempts >= 5)
|
||||
attemptsInfo.nextValidAttempt = now.addSecs(2);
|
||||
else if (attemptsInfo.nbSuccessiveBadAttempts >= 2)
|
||||
attemptsInfo.nextValidAttempt = now.addSecs(1);
|
||||
else
|
||||
attemptsInfo.nextValidAttempt = now;
|
||||
|
||||
LMS_LOG(AUTH, INFO) << "Registering bad attempt for '" << clientAddress.to_string() << "' (" << attemptsInfo.nbSuccessiveBadAttempts << " successive bad attempts)";
|
||||
}
|
||||
|
||||
void
|
||||
LoginThrottler::onGoodClientAttempt(const boost::asio::ip::address& address)
|
||||
{
|
||||
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
|
||||
|
||||
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
|
||||
|
||||
_attemptsInfo.erase(address);
|
||||
}
|
||||
|
||||
bool
|
||||
LoginThrottler::isClientThrottled(const boost::asio::ip::address& address) const
|
||||
{
|
||||
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
|
||||
|
||||
std::shared_lock<std::shared_timed_mutex> lock {_mutex};
|
||||
|
||||
auto it {_attemptsInfo.find(address)};
|
||||
if (it == _attemptsInfo.end())
|
||||
return false;
|
||||
|
||||
return it->second.nextValidAttempt > Wt::WDateTime::currentDateTime();
|
||||
}
|
||||
|
||||
} // Auth
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* This file contains some classes in order to get info from file using the libavconv */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "utils/NetAddress.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class LoginThrottler
|
||||
{
|
||||
public:
|
||||
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
|
||||
|
||||
bool isClientThrottled(const boost::asio::ip::address& address) const;
|
||||
void onBadClientAttempt(const boost::asio::ip::address& address);
|
||||
void onGoodClientAttempt(const boost::asio::ip::address& address);
|
||||
|
||||
private:
|
||||
|
||||
void removeOutdatedEntries();
|
||||
|
||||
struct AttemptsInfo
|
||||
{
|
||||
std::size_t nbSuccessiveBadAttempts {};
|
||||
Wt::WDateTime nextValidAttempt;
|
||||
};
|
||||
|
||||
const std::size_t _maxEntries;
|
||||
|
||||
mutable std::shared_timed_mutex _mutex;
|
||||
std::unordered_map<boost::asio::ip::address, AttemptsInfo> _attemptsInfo;
|
||||
};
|
||||
|
||||
|
||||
} // Auth
|
||||
|
||||
+27
-229
@@ -19,15 +19,6 @@
|
||||
|
||||
#include "Session.hpp"
|
||||
|
||||
#include <Wt/Auth/Dbo/AuthInfo.h>
|
||||
#include <Wt/Auth/Dbo/UserDatabase.h>
|
||||
#include <Wt/Auth/AuthService.h>
|
||||
#include <Wt/Auth/HashFunction.h>
|
||||
#include <Wt/Auth/Identity.h>
|
||||
#include <Wt/Auth/PasswordService.h>
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
#include <Wt/Auth/PasswordVerifier.h>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
@@ -44,12 +35,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 4
|
||||
|
||||
namespace {
|
||||
Wt::Auth::AuthService authService;
|
||||
Wt::Auth::PasswordService passwordService {authService};
|
||||
}
|
||||
#define LMS_DATABASE_VERSION 5
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -112,36 +98,6 @@ Session::doDatabaseMigrationIfNeeded()
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case 3:
|
||||
|
||||
LMS_LOG(DB, INFO) << "Migrating database from version 3...";
|
||||
|
||||
_session.execute(R"(CREATE TABLE IF NOT EXISTS "user_artist_starred" (
|
||||
"user_id" bigint,
|
||||
"artist_id" bigint,
|
||||
primary key ("user_id", "artist_id"),
|
||||
constraint "fk_user_artist_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_user_artist_starred_key2" foreign key ("artist_id") references "artist" ("id") deferrable initially deferred);)");
|
||||
_session.execute(R"(CREATE INDEX "user_artist_starred_user" on "user_artist_starred" ("user_id");)");
|
||||
_session.execute(R"(CREATE INDEX "user_artist_starred_artist" on "user_artist_starred" ("artist_id");)");
|
||||
_session.execute(R"(CREATE TABLE IF NOT EXISTS "user_release_starred" (
|
||||
"user_id" bigint,
|
||||
"release_id" bigint,
|
||||
primary key ("user_id", "release_id"),
|
||||
constraint "fk_user_release_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_user_release_starred_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred);)");
|
||||
_session.execute(R"(CREATE INDEX "user_release_starred_user" on "user_release_starred" ("user_id");)");
|
||||
_session.execute(R"(CREATE INDEX "user_release_starred_release" on "user_release_starred" ("release_id");)");
|
||||
_session.execute(R"(CREATE TABLE IF NOT EXISTS "user_track_starred" (
|
||||
"user_id" bigint,
|
||||
"track_id" bigint,
|
||||
primary key ("user_id", "track_id"),
|
||||
constraint "fk_user_track_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_user_track_starred_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred);)");
|
||||
_session.execute(R"(CREATE INDEX "user_track_starred_user" on "user_track_starred" ("user_id");)");
|
||||
_session.execute(R"(CREATE INDEX "user_track_starred_track" on "user_track_starred" ("track_id");)");
|
||||
break;
|
||||
|
||||
default:
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
throw LmsException {outdatedMsg};
|
||||
@@ -150,51 +106,6 @@ Session::doDatabaseMigrationIfNeeded()
|
||||
VersionInfo::get(*this).modify()->setVersion(LMS_DATABASE_VERSION);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Session::configureAuth(void)
|
||||
{
|
||||
authService.setEmailVerificationEnabled(false);
|
||||
authService.setAuthTokensEnabled(true, "lmsauth");
|
||||
authService.setAuthTokenValidity(24 * 60 * 365); // A year
|
||||
authService.setIdentityPolicy(Wt::Auth::IdentityPolicy::LoginName);
|
||||
authService.setRandomTokenLength(32);
|
||||
|
||||
#if WT_VERSION < 0X04000300
|
||||
authService.setTokenHashFunction(new Wt::Auth::BCryptHashFunction(8));
|
||||
#else
|
||||
authService.setTokenHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(8));
|
||||
#endif
|
||||
|
||||
auto verifier = std::make_unique<Wt::Auth::PasswordVerifier>();
|
||||
verifier->addHashFunction(std::make_unique<Wt::Auth::BCryptHashFunction>(8));
|
||||
passwordService.setVerifier(std::move(verifier));
|
||||
passwordService.setAttemptThrottlingEnabled(true);
|
||||
|
||||
auto strengthValidator = std::make_unique<Wt::Auth::PasswordStrengthValidator>();
|
||||
// Reduce some constraints...
|
||||
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::PassPhrase, 4);
|
||||
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
||||
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
||||
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::ThreeCharClass, 4 );
|
||||
strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::FourCharClass, 4 );
|
||||
|
||||
passwordService.setStrengthValidator(std::move(strengthValidator));
|
||||
}
|
||||
|
||||
const Wt::Auth::AuthService&
|
||||
Session::getAuthService()
|
||||
{
|
||||
return authService;
|
||||
}
|
||||
|
||||
const Wt::Auth::PasswordService&
|
||||
Session::getPasswordService()
|
||||
{
|
||||
return passwordService;
|
||||
}
|
||||
|
||||
|
||||
Session::Session(std::shared_timed_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool)
|
||||
: _mutex {mutex}
|
||||
{
|
||||
@@ -202,75 +113,69 @@ Session::Session(std::shared_timed_mutex& mutex, Wt::Dbo::SqlConnectionPool& con
|
||||
|
||||
_session.mapClass<VersionInfo>("version_info");
|
||||
_session.mapClass<Artist>("artist");
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
_session.mapClass<SimilaritySettings>("similarity_settings");
|
||||
_session.mapClass<SimilaritySettingsFeature>("similarity_settings_feature");
|
||||
|
||||
_session.mapClass<AuthInfo>("auth_info");
|
||||
_session.mapClass<AuthInfo::AuthIdentityType>("auth_identity");
|
||||
_session.mapClass<AuthInfo::AuthTokenType>("auth_token");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<User>("user");
|
||||
|
||||
_users = std::make_unique<UserDatabase>(_session);
|
||||
}
|
||||
|
||||
// TODO make this per database
|
||||
static thread_local bool hasSharedLock {false};
|
||||
static thread_local bool hasUniqueLock {false};
|
||||
|
||||
enum class OwnedLock
|
||||
{
|
||||
None,
|
||||
Shared,
|
||||
Unique,
|
||||
};
|
||||
|
||||
static thread_local std::map<std::shared_timed_mutex*, OwnedLock> lockDebug;
|
||||
|
||||
UniqueTransaction::UniqueTransaction(std::shared_timed_mutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
assert(!hasSharedLock);
|
||||
assert(!hasUniqueLock);
|
||||
hasUniqueLock = true;
|
||||
LMS_LOG(DB, DEBUG) << "UniqueTransaction ACQUIRED";
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::None);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::Unique;
|
||||
}
|
||||
|
||||
UniqueTransaction::~UniqueTransaction()
|
||||
{
|
||||
assert(hasUniqueLock);
|
||||
hasUniqueLock = false;
|
||||
LMS_LOG(DB, DEBUG) << "UniqueTransaction RELEASED";
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::Unique);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::None;
|
||||
}
|
||||
|
||||
SharedTransaction::SharedTransaction(std::shared_timed_mutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
assert(!hasSharedLock);
|
||||
assert(!hasUniqueLock);
|
||||
hasSharedLock = true;
|
||||
LMS_LOG(DB, DEBUG) << "SharedTransaction ACQUIRED";
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::None);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::Shared;
|
||||
}
|
||||
|
||||
SharedTransaction::~SharedTransaction()
|
||||
{
|
||||
assert(hasSharedLock);
|
||||
hasSharedLock = false;
|
||||
LMS_LOG(DB, DEBUG) << "SharedTransaction RELEASED";
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::Shared);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::None;
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkUniqueLocked()
|
||||
{
|
||||
assert(hasUniqueLock);
|
||||
assert(lockDebug[&_mutex] == OwnedLock::Unique);
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkSharedLocked()
|
||||
{
|
||||
assert(hasUniqueLock || hasSharedLock);
|
||||
assert(lockDebug[&_mutex] != OwnedLock::None);
|
||||
}
|
||||
|
||||
std::unique_ptr<UniqueTransaction>
|
||||
@@ -307,6 +212,7 @@ Session::prepareTables()
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
@@ -346,112 +252,4 @@ Session::optimize()
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
|
||||
std::string
|
||||
Session::getUserLoginName(Wt::Dbo::ptr<User> user)
|
||||
{
|
||||
const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))};
|
||||
if (!authUser.isValid())
|
||||
throw LmsException {"Invalid user state"};
|
||||
|
||||
return authUser.identity(Wt::Auth::Identity::LoginName).toUTF8();
|
||||
}
|
||||
|
||||
bool
|
||||
Session::checkUserPassword(const std::string& loginName, const std::string& password)
|
||||
{
|
||||
auto transaction {createUniqueTransaction()};
|
||||
|
||||
auto authUser {_users->findWithIdentity(Wt::Auth::Identity::LoginName, loginName)};
|
||||
if (!authUser.isValid())
|
||||
return false; // TODO const time?
|
||||
|
||||
return passwordService.verifyPassword(authUser, password) == Wt::Auth::PasswordResult::PasswordValid;
|
||||
}
|
||||
|
||||
void
|
||||
Session::updateUserPassword(Wt::Dbo::ptr<User> user, const std::string& password)
|
||||
{
|
||||
const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))};
|
||||
if (!authUser.isValid())
|
||||
throw LmsException {"Bad user state"};
|
||||
passwordService.updatePassword(authUser, password);
|
||||
}
|
||||
|
||||
Wt::WDateTime
|
||||
Session::getUserLastLoginAttempt(Wt::Dbo::ptr<User> user)
|
||||
{
|
||||
const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))};
|
||||
if (!authUser.isValid())
|
||||
throw LmsException {"Bad user state"};
|
||||
|
||||
return authUser.lastLoginAttempt();
|
||||
}
|
||||
|
||||
void
|
||||
Session::removeUser(Database::User::pointer user)
|
||||
{
|
||||
checkUniqueLocked();
|
||||
|
||||
auto authUser = _users->findWithId(std::to_string(user.id()));
|
||||
_users->deleteUser(authUser);
|
||||
user.remove();
|
||||
}
|
||||
|
||||
Wt::Auth::AbstractUserDatabase&
|
||||
Session::getUserDatabase()
|
||||
{
|
||||
return *_users;
|
||||
}
|
||||
|
||||
User::pointer
|
||||
Session::createUser(const std::string& loginName, const std::string& password)
|
||||
{
|
||||
Wt::Auth::User authUser {_users->registerNew()};
|
||||
if (!authUser.isValid())
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Invalid authUser";
|
||||
return {};
|
||||
}
|
||||
User::pointer user {User::create(*this)};
|
||||
Wt::Dbo::ptr<AuthInfo> authInfo = _users->find(authUser);
|
||||
authInfo.modify()->setUser(user);
|
||||
|
||||
authUser.setIdentity(Wt::Auth::Identity::LoginName, loginName);
|
||||
passwordService.updatePassword(authUser, password);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
User::pointer
|
||||
Session::getLoggedUser()
|
||||
{
|
||||
if (_login.loggedIn())
|
||||
return getUser(_login.user());
|
||||
else
|
||||
return User::pointer();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
Session::getUser(const Wt::Auth::User& authUser)
|
||||
{
|
||||
if (!authUser.isValid()) {
|
||||
LMS_LOG(DB, ERROR) << "Session::getUser: invalid authUser";
|
||||
return User::pointer();
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<AuthInfo> authInfo = _users->find(authUser);
|
||||
|
||||
return authInfo->user();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
Session::getUser(const std::string& loginName)
|
||||
{
|
||||
auto authUser {getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, loginName)};
|
||||
if (!authUser.isValid())
|
||||
return User::pointer {};
|
||||
|
||||
return getUser(authUser);
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -26,19 +26,9 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
#include <Wt/Auth/Dbo/AuthInfo.h>
|
||||
|
||||
#include <Wt/Auth/Dbo/UserDatabase.h>
|
||||
#include <Wt/Auth/Login.h>
|
||||
#include <Wt/Auth/PasswordService.h>
|
||||
|
||||
#include "User.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
using AuthInfo = Wt::Auth::Dbo::AuthInfo<User>;
|
||||
using UserDatabase = Wt::Auth::Dbo::UserDatabase<AuthInfo>;
|
||||
|
||||
class UniqueTransaction
|
||||
{
|
||||
public:
|
||||
@@ -81,24 +71,6 @@ class Session
|
||||
|
||||
void optimize();
|
||||
|
||||
// User management
|
||||
Wt::Dbo::ptr<User> getLoggedUser(); // get the current user, may return empty
|
||||
Wt::Dbo::ptr<User> getUser(const std::string& loginName);
|
||||
std::string getUserLoginName(Wt::Dbo::ptr<User> user);
|
||||
Wt::Dbo::ptr<User> createUser(const std::string& loginName, const std::string& password);
|
||||
void removeUser(Wt::Dbo::ptr<User> user);
|
||||
bool checkUserPassword(const std::string& loginName, const std::string& password);
|
||||
void updateUserPassword(Wt::Dbo::ptr<User> user, const std::string& password);
|
||||
Wt::WDateTime getUserLastLoginAttempt(Wt::Dbo::ptr<User> user);
|
||||
|
||||
Wt::Auth::AbstractUserDatabase& getUserDatabase();
|
||||
Wt::Auth::Login& getLogin() { return _login; } // TODO move
|
||||
|
||||
// Long living shared associated services
|
||||
static void configureAuth();
|
||||
static const Wt::Auth::AuthService& getAuthService();
|
||||
static const Wt::Auth::PasswordService& getPasswordService();
|
||||
|
||||
Wt::Dbo::Session& getDboSession() { return _session; }
|
||||
|
||||
private:
|
||||
@@ -109,13 +81,8 @@ class Session
|
||||
void doDatabaseMigrationIfNeeded();
|
||||
void prepareTables(); // need to run only once at startup
|
||||
|
||||
Wt::Dbo::ptr<User> getUser(const Wt::Auth::User& authUser);
|
||||
|
||||
std::shared_timed_mutex& _mutex;
|
||||
Wt::Dbo::Session _session;
|
||||
std::unique_ptr<UserDatabase> _users;
|
||||
Wt::Auth::Login _login;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
+54
-2
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "User.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "Artist.hpp"
|
||||
#include "Release.hpp"
|
||||
#include "Session.hpp"
|
||||
@@ -27,6 +28,36 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
|
||||
AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {user}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
auto res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::getByValue(Session& session, const std::string& value)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value);
|
||||
}
|
||||
|
||||
static const std::string playedListName {"__played_tracks__"};
|
||||
static const std::string queuedListName {"__queued_tracks__"};
|
||||
|
||||
@@ -46,6 +77,14 @@ User::User()
|
||||
|
||||
}
|
||||
|
||||
User::User(const std::string& loginName, const PasswordHash& passwordHash)
|
||||
: User()
|
||||
{
|
||||
_loginName = loginName;
|
||||
_passwordHash = passwordHash.hash;
|
||||
_passwordSalt = passwordHash.salt;
|
||||
}
|
||||
|
||||
std::vector<User::pointer>
|
||||
User::getAll(Session& session)
|
||||
{
|
||||
@@ -65,11 +104,11 @@ User::getDemo(Session& session)
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::create(Session& session)
|
||||
User::create(Session& session, const std::string& loginName, const PasswordHash& passwordHash)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
User::pointer user {session.getDboSession().add(std::make_unique<User>())};
|
||||
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName, passwordHash))};
|
||||
|
||||
TrackList::create(session, playedListName, TrackList::Type::Internal, false, user);
|
||||
TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user);
|
||||
@@ -85,6 +124,13 @@ User::getById(Session& session, IdType id)
|
||||
return session.getDboSession().find<User>().where("id = ?").bind( id );
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getByLoginName(Session& session, const std::string& name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name);
|
||||
}
|
||||
|
||||
void
|
||||
User::setAudioTranscodeBitrate(Bitrate bitrate)
|
||||
{
|
||||
@@ -99,6 +145,12 @@ User::setMaxAudioTranscodeBitrate(Bitrate bitrate)
|
||||
_audioTranscodeBitrate = _maxAudioTranscodeBitrate;
|
||||
}
|
||||
|
||||
void
|
||||
User::clearAuthTokens()
|
||||
{
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
Bitrate
|
||||
User::getAudioTranscodeBitrate(void) const
|
||||
{
|
||||
|
||||
+84
-23
@@ -22,6 +22,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
@@ -46,6 +47,41 @@ enum class AudioFormat
|
||||
|
||||
using Bitrate = std::size_t;
|
||||
|
||||
class User;
|
||||
class AuthToken
|
||||
{
|
||||
public:
|
||||
|
||||
using pointer = Wt::Dbo::ptr<AuthToken>;
|
||||
|
||||
AuthToken() = default;
|
||||
AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr<User> user);
|
||||
|
||||
// Utility
|
||||
static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr<User> user);
|
||||
static void removeExpiredTokens(Session& session, Wt::WDateTime now);
|
||||
static pointer getByValue(Session& session, const std::string& value);
|
||||
|
||||
// Accessors
|
||||
Wt::Dbo::ptr<User> getUser() const { return _user; }
|
||||
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _value, "value");
|
||||
Wt::Dbo::field(a, _expiry, "expiry");
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
std::string _value;
|
||||
Wt::WDateTime _expiry;
|
||||
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
|
||||
class User : public Wt::Dbo::Dbo<User>
|
||||
{
|
||||
public:
|
||||
@@ -61,21 +97,34 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
DEMO
|
||||
};
|
||||
|
||||
struct PasswordHash
|
||||
{
|
||||
std::string salt;
|
||||
std::string hash;
|
||||
};
|
||||
|
||||
// list of audio parameters
|
||||
static const std::set<Bitrate> audioTranscodeAllowedBitrates;
|
||||
|
||||
User();
|
||||
User(const std::string& loginName, const PasswordHash& passwordHash);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session);
|
||||
static pointer create(Session& session, const std::string& loginName, const PasswordHash& passwordHash);
|
||||
|
||||
// accessors
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getByLoginName(const std::string& loginName);
|
||||
static pointer getByLoginName(Session& session, const std::string& loginName);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static pointer getDemo(Session& session);
|
||||
|
||||
// accessors
|
||||
const std::string& getLoginName() const { return _loginName; }
|
||||
PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; }
|
||||
Wt::WDateTime getLastLogin() const { return _lastLogin; }
|
||||
|
||||
// write
|
||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
|
||||
void setType(Type type) { _type = type; }
|
||||
void setAudioTranscodeEnable(bool value) { _audioTranscodeEnable = value; }
|
||||
void setAudioTranscodeFormat(AudioFormat format) { _audioTranscodeFormat = format; }
|
||||
@@ -84,6 +133,7 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
||||
void setRadio(bool val) { _radio = val; }
|
||||
void setRepeatAll(bool val) { _repeatAll = val; }
|
||||
void clearAuthTokens();
|
||||
|
||||
// read
|
||||
bool isAdmin() const { return _type == Type::ADMIN; }
|
||||
@@ -115,22 +165,27 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
std::vector<Wt::Dbo::ptr<Track>> getStarredTracks() const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate");
|
||||
Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable");
|
||||
Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate");
|
||||
Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format");
|
||||
// User's dynamic data
|
||||
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
||||
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
||||
Wt::Dbo::field(a, _radio, "radio");
|
||||
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _loginName, "login_name");
|
||||
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
||||
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
||||
Wt::Dbo::field(a, _lastLogin, "last_login");
|
||||
Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate");
|
||||
Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable");
|
||||
Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate");
|
||||
Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format");
|
||||
// User's dynamic data
|
||||
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
||||
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
||||
Wt::Dbo::field(a, _radio, "radio");
|
||||
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -138,16 +193,21 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
static const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS};
|
||||
static const Bitrate defaultAudioTranscodeBitrate {128000};
|
||||
|
||||
std::string _loginName;
|
||||
std::string _passwordSalt;
|
||||
std::string _passwordHash;
|
||||
Wt::WDateTime _lastLogin;
|
||||
|
||||
// Admin defined settings
|
||||
int _maxAudioTranscodeBitrate;
|
||||
Type _type {Type::REGULAR};
|
||||
|
||||
// User defined settings
|
||||
bool _audioTranscodeEnable {defaultAudioTranscodeEnable};
|
||||
AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat};
|
||||
int _audioTranscodeBitrate {defaultAudioTranscodeBitrate};
|
||||
bool _audioTranscodeEnable {defaultAudioTranscodeEnable};
|
||||
AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat};
|
||||
int _audioTranscodeBitrate {defaultAudioTranscodeBitrate};
|
||||
|
||||
// User's dynamic data
|
||||
// User's dynamic data (UI)
|
||||
int _curPlayingTrackPos {}; // Current track position in queue
|
||||
bool _repeatAll {};
|
||||
bool _radio {};
|
||||
@@ -156,6 +216,7 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _starredReleases;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _starredTracks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
||||
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@
|
||||
#include "api/subsonic/SubsonicResource.hpp"
|
||||
#include "av/AvInfo.hpp"
|
||||
#include "av/AvTranscoder.hpp"
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "scanner/MediaScanner.hpp"
|
||||
@@ -121,7 +122,6 @@ int main(int argc, char* argv[])
|
||||
Image::init(argv[0]);
|
||||
Av::AvInit();
|
||||
Av::Transcoder::init();
|
||||
Database::Session::configureAuth();
|
||||
|
||||
// Initializing a connection pool to the database that will be shared along services
|
||||
Database::Database database {Config::instance().getPath("working-dir") / "lms.db"};
|
||||
@@ -129,6 +129,7 @@ int main(int argc, char* argv[])
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
// Service initialization order is important
|
||||
ServiceProvider<Auth::AuthService>::create(Config::instance().getULong("login-throttler-max-entriees", 10000));
|
||||
Scanner::MediaScanner& mediaScanner {ServiceProvider<Scanner::MediaScanner>::create(database.createSession())};
|
||||
|
||||
Similarity::FeaturesScannerAddon similarityFeaturesScannerAddon {database.createSession()};
|
||||
|
||||
@@ -408,11 +408,10 @@ MediaScanner::scan(boost::system::error_code err)
|
||||
scanMediaDirectory(_mediaDirectory, forceScan, stats);
|
||||
LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "' DONE";
|
||||
|
||||
removeOrphanEntries();
|
||||
|
||||
if (_running)
|
||||
{
|
||||
removeOrphanEntries();
|
||||
checkDuplicatedAudioFiles(stats);
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")";
|
||||
|
||||
|
||||
@@ -167,6 +167,9 @@ getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std::
|
||||
continue;
|
||||
|
||||
res.push_back(trackArtist.id());
|
||||
|
||||
if (res.size() == maxCount)
|
||||
break;
|
||||
}
|
||||
|
||||
if (res.size() == maxCount)
|
||||
|
||||
+192
-47
@@ -19,82 +19,227 @@
|
||||
|
||||
#include "Auth.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
#include <Wt/WFormModel.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
Auth::Auth()
|
||||
: Wt::WTemplateFormView(Wt::WString::tr("Lms.Auth.template"))
|
||||
{
|
||||
_model = std::make_shared<Wt::Auth::AuthModel>(LmsApp->getDbSession().getAuthService(), LmsApp->getDbSession().getUserDatabase());
|
||||
_model->addPasswordAuth(&Database::Session::getPasswordService());
|
||||
static const std::string authCookieName {"LmsAuth"};
|
||||
|
||||
// LoginName
|
||||
setFormWidget(Wt::Auth::AuthModel::LoginNameField, std::make_unique<Wt::WLineEdit>());
|
||||
static
|
||||
std::string
|
||||
createSecret()
|
||||
{
|
||||
std::array<std::uint8_t, 32> buffer;
|
||||
fillRandom(buffer);
|
||||
|
||||
std::ostringstream oss;
|
||||
for (std::uint8_t b : buffer)
|
||||
oss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(b);
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
createAuthToken(Database::IdType userId)
|
||||
{
|
||||
|
||||
const std::string secret {createSecret()};
|
||||
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
|
||||
const Wt::WDateTime expiry {now.addYears(1)};
|
||||
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
auto demoUser = Database::User::getDemo(LmsApp->getDbSession());
|
||||
if (demoUser)
|
||||
{
|
||||
const std::string userName {LmsApp->getDbSession().getUserLoginName(demoUser)};
|
||||
_model->setValue(Wt::Auth::AuthModel::LoginNameField, userName );
|
||||
_model->setValue(Wt::Auth::AuthModel::PasswordField, userName);
|
||||
}
|
||||
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)};
|
||||
Database::AuthToken::create(LmsApp->getDbSession(), secret, expiry, user);
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
||||
}
|
||||
|
||||
LmsApp->setCookie(authCookieName,
|
||||
secret,
|
||||
expiry.toTime_t() - now.toTime_t(),
|
||||
"",
|
||||
"",
|
||||
LmsApp->environment().urlScheme() == "https");
|
||||
}
|
||||
|
||||
|
||||
boost::optional<Database::IdType>
|
||||
processAuthToken(const Wt::WEnvironment& env)
|
||||
{
|
||||
const std::string* authCookie {env.getCookie(authCookieName)};
|
||||
if (!authCookie)
|
||||
return boost::none;
|
||||
|
||||
Database::IdType userId {};
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(LmsApp->getDbSession(), *authCookie)};
|
||||
if (!authToken)
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Client '" << env.clientAddress() << "' presented a token that has not been found";
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "Expired auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
authToken.remove();
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
userId = authToken->getUser().id();
|
||||
|
||||
authToken.remove();
|
||||
}
|
||||
|
||||
createAuthToken(userId);
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
class AuthModel : public Wt::WFormModel
|
||||
{
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field LoginNameField;
|
||||
static const Field PasswordField;
|
||||
static const Field RememberMeField;
|
||||
|
||||
AuthModel()
|
||||
{
|
||||
addField(LoginNameField);
|
||||
addField(PasswordField);
|
||||
addField(RememberMeField);
|
||||
|
||||
setValidator(LoginNameField, createNameValidator());
|
||||
setValidator(PasswordField, createMandatoryValidator());
|
||||
}
|
||||
|
||||
|
||||
void saveData()
|
||||
{
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())};
|
||||
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
|
||||
_userId = user.id();
|
||||
}
|
||||
|
||||
if (Wt::asNumber(value(RememberMeField)))
|
||||
createAuthToken(*_userId);
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
Wt::WString error;
|
||||
|
||||
if (field == PasswordField)
|
||||
{
|
||||
switch (getService<::Auth::AuthService>()->checkUserPassword(
|
||||
LmsApp->getDbSession(),
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
valueText(LoginNameField).toUTF8(),
|
||||
valueText(PasswordField).toUTF8()))
|
||||
{
|
||||
case ::Auth::AuthService::PasswordCheckResult::Match:
|
||||
break;
|
||||
case ::Auth::AuthService::PasswordCheckResult::Mismatch:
|
||||
error = Wt::WString::tr("Lms.password-bad-login-combination");
|
||||
break;
|
||||
case ::Auth::AuthService::PasswordCheckResult::Throttled:
|
||||
error = Wt::WString::tr("Lms.password-client-throttled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
|
||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error));
|
||||
|
||||
return (validation(field).state() == Wt::ValidationState::Valid);
|
||||
}
|
||||
|
||||
boost::optional<Database::IdType> getUserId() const { return _userId; }
|
||||
|
||||
private:
|
||||
|
||||
boost::optional<Database::IdType> _userId;
|
||||
};
|
||||
|
||||
const AuthModel::Field AuthModel::LoginNameField {"login-name"};
|
||||
const AuthModel::Field AuthModel::PasswordField {"password"};
|
||||
const AuthModel::Field AuthModel::RememberMeField {"remember-me"};
|
||||
|
||||
|
||||
Auth::Auth()
|
||||
: Wt::WTemplateFormView {Wt::WString::tr("Lms.Auth.template")}
|
||||
{
|
||||
auto model {std::make_shared<AuthModel>()};
|
||||
|
||||
auto processAuth = [=]()
|
||||
{
|
||||
updateModel(model.get());
|
||||
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
userLoggedIn.emit(*model->getUserId());
|
||||
}
|
||||
else
|
||||
updateView(model.get());
|
||||
};
|
||||
|
||||
// LoginName
|
||||
setFormWidget(AuthModel::LoginNameField, std::make_unique<Wt::WLineEdit>());
|
||||
|
||||
// Password
|
||||
auto password = std::make_unique<Wt::WLineEdit>();
|
||||
password->setEchoMode(Wt::EchoMode::Password);
|
||||
password->enterPressed().connect(this, &Auth::processAuth);
|
||||
setFormWidget(Wt::Auth::AuthModel::PasswordField, std::move(password));
|
||||
password->enterPressed().connect(this, processAuth);
|
||||
setFormWidget(AuthModel::PasswordField, std::move(password));
|
||||
|
||||
// Remember Me
|
||||
setFormWidget(Wt::Auth::AuthModel::RememberMeField, std::make_unique<Wt::WCheckBox>());
|
||||
// Remember me
|
||||
setFormWidget(AuthModel::RememberMeField, std::make_unique<Wt::WCheckBox>());
|
||||
|
||||
Wt::WPushButton* loginBtn = bindNew<Wt::WPushButton>("login-btn", Wt::WString::tr("Lms.login"));
|
||||
loginBtn->clicked().connect(this, &Auth::processAuth);
|
||||
|
||||
LmsApp->getDbSession().getLogin().changed().connect(std::bind([=]
|
||||
{
|
||||
if (LmsApp->getDbSession().getLogin().loggedIn())
|
||||
this->setHidden(true);
|
||||
}));
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
updateView(_model.get());
|
||||
|
||||
Wt::Auth::User user = _model->processAuthToken();
|
||||
if (user.isValid())
|
||||
{
|
||||
LMS_LOG(UI, DEBUG) << "Valid user found from auth token (id = " << user.id() << ")";
|
||||
_model->loginUser(LmsApp->getDbSession().getLogin(), user, Wt::Auth::LoginState::Weak);
|
||||
Database::User::pointer demoUser {Database::User::getDemo(LmsApp->getDbSession())};
|
||||
if (demoUser)
|
||||
{
|
||||
model->setValue(AuthModel::LoginNameField, demoUser->getLoginName());
|
||||
model->setValue(AuthModel::PasswordField, demoUser->getLoginName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Auth::processAuth()
|
||||
{
|
||||
updateModel(_model.get());
|
||||
Wt::WPushButton* loginBtn {bindNew<Wt::WPushButton>("login-btn", Wt::WString::tr("Lms.login"))};
|
||||
loginBtn->clicked().connect(this, processAuth);
|
||||
|
||||
if (_model->validate())
|
||||
_model->login(LmsApp->getDbSession().getLogin());
|
||||
else
|
||||
updateView(_model.get());
|
||||
}
|
||||
updateView(model.get());
|
||||
|
||||
void
|
||||
Auth::logout()
|
||||
{
|
||||
_model->logout(LmsApp->getDbSession().getLogin());
|
||||
}
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
+9
-7
@@ -19,22 +19,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
#include <Wt/Auth/AuthModel.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
|
||||
// If success, returns the authenticated user id
|
||||
boost::optional<Database::IdType> processAuthToken(const Wt::WEnvironment& env);
|
||||
|
||||
class Auth : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
Auth();
|
||||
|
||||
void logout();
|
||||
|
||||
private:
|
||||
void processAuth();
|
||||
|
||||
std::shared_ptr<Wt::Auth::AuthModel> _model;
|
||||
Wt::Signal<Database::IdType /*userId*/> userLoggedIn;
|
||||
};
|
||||
|
||||
|
||||
|
||||
+82
-38
@@ -28,13 +28,13 @@
|
||||
#include <Wt/WServer.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WText.h>
|
||||
#include <Wt/Auth/Identity.h>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "cover/CoverArtGrabber.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "explore/Explore.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "admin/UsersView.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "resource/AudioResource.hpp"
|
||||
#include "Auth.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "PlayHistoryView.hpp"
|
||||
#include "PlayQueueView.hpp"
|
||||
@@ -66,6 +67,39 @@ LmsApplication::instance()
|
||||
return reinterpret_cast<LmsApplication*>(Wt::WApplication::instance());
|
||||
}
|
||||
|
||||
Wt::Dbo::ptr<Database::User>
|
||||
LmsApplication::getUser() const
|
||||
{
|
||||
if (!_userId)
|
||||
return {};
|
||||
|
||||
return Database::User::getById(*_dbSession, *_userId);
|
||||
}
|
||||
|
||||
bool
|
||||
LmsApplication::isUserAdmin() const
|
||||
{
|
||||
auto transaction {_dbSession->createSharedTransaction()};
|
||||
|
||||
return getUser()->isAdmin();
|
||||
}
|
||||
|
||||
bool
|
||||
LmsApplication::isUserDemo() const
|
||||
{
|
||||
auto transaction {_dbSession->createSharedTransaction()};
|
||||
|
||||
return getUser()->isDemo();
|
||||
}
|
||||
|
||||
std::string
|
||||
LmsApplication::getUserLoginName() const
|
||||
{
|
||||
auto transaction {_dbSession->createSharedTransaction()};
|
||||
|
||||
return getUser()->getLoginName();
|
||||
}
|
||||
|
||||
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
std::unique_ptr<Database::Session> dbSession,
|
||||
LmsApplicationGroupContainer& appGroups)
|
||||
@@ -131,25 +165,35 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
if (firstConnection)
|
||||
{
|
||||
root()->addWidget(std::make_unique<InitWizardView>());
|
||||
return;
|
||||
}
|
||||
|
||||
auto userId {processAuthToken(env)};
|
||||
if (userId)
|
||||
{
|
||||
handleUserLoggedIn(*userId);
|
||||
}
|
||||
else
|
||||
{
|
||||
LmsApp->getDbSession().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent);
|
||||
_auth = root()->addNew<Auth>();
|
||||
Auth* auth {root()->addNew<Auth>()};
|
||||
auth->userLoggedIn.connect(this, &LmsApplication::handleUserLoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::finalize()
|
||||
{
|
||||
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
|
||||
|
||||
getApplicationGroup().postOthers([info]
|
||||
if (_userId)
|
||||
{
|
||||
LmsApp->getEvents().appClosed(info);
|
||||
});
|
||||
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
|
||||
|
||||
getApplicationGroup().leave();
|
||||
getApplicationGroup().postOthers([info]
|
||||
{
|
||||
LmsApp->getEvents().appClosed(info);
|
||||
});
|
||||
|
||||
getApplicationGroup().leave();
|
||||
}
|
||||
|
||||
preQuit().emit();
|
||||
}
|
||||
@@ -280,28 +324,37 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin)
|
||||
LmsApplicationGroup&
|
||||
LmsApplication::getApplicationGroup()
|
||||
{
|
||||
return _appGroups.get(_userIdentity);
|
||||
return _appGroups.get(*_userId);
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleAuthEvent()
|
||||
LmsApplication::handleUserLoggedOut()
|
||||
{
|
||||
if (!getDbSession().getLogin().loggedIn())
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "User '" << _userIdentity << " 'logged out, session = " << sessionId();
|
||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
|
||||
|
||||
goHomeAndQuit();
|
||||
return;
|
||||
{
|
||||
auto transaction {_dbSession->createUniqueTransaction()};
|
||||
getUser().modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
setConfirmCloseMessage("");
|
||||
goHomeAndQuit();
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleUserLoggedIn(Database::IdType userId)
|
||||
{
|
||||
_userId = userId;
|
||||
|
||||
root()->clear();
|
||||
|
||||
try
|
||||
{
|
||||
// post([this]
|
||||
// {
|
||||
_userIdentity = getAuthUser().identity(Wt::Auth::Identity::LoginName);
|
||||
const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())};
|
||||
|
||||
LMS_LOG(UI, INFO) << "User '" << _userIdentity << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", session = " << sessionId();
|
||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent();
|
||||
getApplicationGroup().join(info);
|
||||
|
||||
getApplicationGroup().postOthers([info]
|
||||
@@ -310,7 +363,8 @@ LmsApplication::handleAuthEvent()
|
||||
});
|
||||
|
||||
createHome();
|
||||
triggerUpdate();
|
||||
|
||||
// triggerUpdate();
|
||||
// });
|
||||
}
|
||||
catch (std::exception& e)
|
||||
@@ -323,12 +377,6 @@ LmsApplication::handleAuthEvent()
|
||||
void
|
||||
LmsApplication::createHome()
|
||||
{
|
||||
|
||||
{
|
||||
auto transaction {_dbSession->createSharedTransaction()};
|
||||
_isAdmin = LmsApp->getUser()->isAdmin();
|
||||
}
|
||||
|
||||
_imageResource = std::make_shared<ImageResource>();
|
||||
_audioResource = std::make_shared<AudioResource>();
|
||||
|
||||
@@ -337,8 +385,8 @@ LmsApplication::createHome()
|
||||
Wt::WTemplate* main {root()->addWidget(std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.template")))};
|
||||
|
||||
// Navbar
|
||||
Wt::WNavigationBar* navbar = main->bindNew<Wt::WNavigationBar>("navbar-top");
|
||||
navbar->setTitle("LMS", Wt::WLink(Wt::LinkType::InternalPath, "/artists"));
|
||||
Wt::WNavigationBar* navbar {main->bindNew<Wt::WNavigationBar>("navbar-top")};
|
||||
navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"});
|
||||
navbar->setResponsive(true);
|
||||
|
||||
Wt::WMenu* menu {navbar->addMenu(std::make_unique<Wt::WMenu>())};
|
||||
@@ -370,7 +418,7 @@ LmsApplication::createHome()
|
||||
|
||||
Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique<Wt::WMenu>(), Wt::AlignmentFlag::Right);
|
||||
std::size_t itemCounter = 0;
|
||||
if (_isAdmin)
|
||||
if (isUserAdmin())
|
||||
{
|
||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration"));
|
||||
menuItem->setSelectable(false);
|
||||
@@ -395,11 +443,7 @@ LmsApplication::createHome()
|
||||
{
|
||||
auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout"));
|
||||
menuItem->setSelectable(true);
|
||||
menuItem->triggered().connect(std::bind([=]
|
||||
{
|
||||
setConfirmCloseMessage("");
|
||||
_auth->logout();
|
||||
}));
|
||||
menuItem->triggered().connect(this, &LmsApplication::handleUserLoggedOut);
|
||||
}
|
||||
|
||||
// Contents
|
||||
@@ -412,7 +456,7 @@ LmsApplication::createHome()
|
||||
mainStack->addNew<SettingsView>();
|
||||
|
||||
// Admin stuff
|
||||
if (_isAdmin)
|
||||
if (isUserAdmin())
|
||||
{
|
||||
mainStack->addNew<DatabaseSettingsView>();
|
||||
mainStack->addNew<UsersView>();
|
||||
@@ -493,7 +537,7 @@ LmsApplication::createHome()
|
||||
|
||||
_events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats)
|
||||
{
|
||||
if (_isAdmin)
|
||||
if (isUserAdmin())
|
||||
{
|
||||
notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete")
|
||||
.arg(static_cast<unsigned>(stats.nbFiles()))
|
||||
@@ -509,7 +553,7 @@ LmsApplication::createHome()
|
||||
_events.appOpen.connect([=] (LmsApplicationInfo info)
|
||||
{
|
||||
// Only one active session by user
|
||||
if (!LmsApp->getUser()->isDemo())
|
||||
if (!LmsApp->isUserDemo())
|
||||
{
|
||||
setConfirmCloseMessage("");
|
||||
quit(Wt::WString::tr("Lms.quit-other-session"));
|
||||
@@ -518,10 +562,10 @@ LmsApplication::createHome()
|
||||
|
||||
internalPathChanged().connect(std::bind([=]
|
||||
{
|
||||
handlePathChange(mainStack, _isAdmin);
|
||||
handlePathChange(mainStack, isUserAdmin());
|
||||
}));
|
||||
|
||||
handlePathChange(mainStack, _isAdmin);
|
||||
handlePathChange(mainStack, isUserAdmin());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include "scanner/MediaScanner.hpp"
|
||||
|
||||
#include "LmsApplicationGroup.hpp"
|
||||
#include "Auth.hpp"
|
||||
|
||||
namespace Database {
|
||||
class Artist;
|
||||
@@ -41,6 +40,7 @@ namespace UserInterface {
|
||||
|
||||
class AudioResource;
|
||||
class ImageResource;
|
||||
class Auth;
|
||||
|
||||
// Events that can be listen from anywhere in the application
|
||||
struct Events
|
||||
@@ -83,9 +83,10 @@ class LmsApplication : public Wt::WApplication
|
||||
std::shared_ptr<AudioResource> getAudioResource() { return _audioResource; }
|
||||
Database::Session& getDbSession() { return *_dbSession.get();}
|
||||
|
||||
const Wt::Auth::User& getAuthUser() { return getDbSession().getLogin().user(); }
|
||||
Wt::Dbo::ptr<Database::User> getUser() { return getDbSession().getLoggedUser(); }
|
||||
Wt::WString getUserIdentity() { return _userIdentity; }
|
||||
Wt::Dbo::ptr<Database::User> getUser() const;
|
||||
bool isUserAdmin() const; // user must be logged in prior this call
|
||||
bool isUserDemo() const; // user must be logged in prior this call
|
||||
std::string getUserLoginName() const; // user must be logged in prior this call
|
||||
|
||||
Events& getEvents() { return _events; }
|
||||
|
||||
@@ -109,8 +110,10 @@ class LmsApplication : public Wt::WApplication
|
||||
|
||||
LmsApplicationGroup& getApplicationGroup();
|
||||
|
||||
// Events
|
||||
void handleAuthEvent();
|
||||
// Signal slots
|
||||
void handleUserLoggedOut();
|
||||
void handleUserLoggedIn(Database::IdType userId);
|
||||
|
||||
void notify(const Wt::WEvent& event) override;
|
||||
void finalize() override;
|
||||
|
||||
@@ -120,11 +123,9 @@ class LmsApplication : public Wt::WApplication
|
||||
std::unique_ptr<Database::Session> _dbSession;
|
||||
LmsApplicationGroupContainer& _appGroups;
|
||||
Events _events;
|
||||
Wt::WString _userIdentity;
|
||||
Auth* _auth {};
|
||||
boost::optional<Database::IdType> _userId {};
|
||||
std::shared_ptr<ImageResource> _imageResource;
|
||||
std::shared_ptr<AudioResource> _audioResource;
|
||||
bool _isAdmin {};
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ LmsApplicationInfo::fromEnvironment(const Wt::WEnvironment& env)
|
||||
void
|
||||
LmsApplicationGroup::join(LmsApplicationInfo info)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::unique_lock<std::mutex> lock {_mutex};
|
||||
|
||||
_apps.emplace(wApp->sessionId(), std::move(info));
|
||||
}
|
||||
@@ -45,7 +45,7 @@ LmsApplicationGroup::join(LmsApplicationInfo info)
|
||||
void
|
||||
LmsApplicationGroup::leave()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::unique_lock<std::mutex> lock {_mutex};
|
||||
|
||||
_apps.erase(wApp->sessionId());
|
||||
}
|
||||
@@ -55,7 +55,7 @@ LmsApplicationGroup::getOtherSessionIds() const
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::unique_lock<std::mutex> lock {_mutex};
|
||||
for (auto const& app : _apps)
|
||||
{
|
||||
if (app.first != wApp->sessionId())
|
||||
@@ -79,11 +79,11 @@ LmsApplicationGroup::postOthers(std::function<void()> func) const
|
||||
}
|
||||
|
||||
LmsApplicationGroup&
|
||||
LmsApplicationGroupContainer::get(Wt::WString identity)
|
||||
LmsApplicationGroupContainer::get(Database::IdType userId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
std::unique_lock<std::mutex> lock {_mutex};
|
||||
|
||||
return _apps[identity];
|
||||
return _apps[userId];
|
||||
}
|
||||
|
||||
} // UserInterface
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/WEnvironment.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
|
||||
@@ -56,10 +58,10 @@ class LmsApplicationGroup
|
||||
class LmsApplicationGroupContainer
|
||||
{
|
||||
public:
|
||||
LmsApplicationGroup& get(Wt::WString identity);
|
||||
LmsApplicationGroup& get(Database::IdType userId);
|
||||
|
||||
private:
|
||||
std::map<Wt::WString, LmsApplicationGroup> _apps;
|
||||
std::map<Database::IdType /* userId */, LmsApplicationGroup> _apps;
|
||||
std::mutex _mutex;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "similarity/SimilaritySearcher.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -131,7 +132,14 @@ PlayQueue::PlayQueue()
|
||||
{
|
||||
LmsApp->post([=]
|
||||
{
|
||||
load(LmsApp->getUser()->getCurPlayingTrackPos(), false);
|
||||
std::size_t trackPos {};
|
||||
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
trackPos = LmsApp->getUser()->getCurPlayingTrackPos();
|
||||
}
|
||||
|
||||
load(trackPos, false);
|
||||
});
|
||||
trackList = LmsApp->getUser()->getQueuedTrackList(LmsApp->getDbSession());
|
||||
}
|
||||
|
||||
+16
-9
@@ -24,12 +24,15 @@
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include <Wt/WFormModel.h>
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -69,20 +72,27 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
void saveData()
|
||||
{
|
||||
Database::User::PasswordHash passwordHash;
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
passwordHash = getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
LmsApp->getUser().modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField)));
|
||||
Database::User::pointer user {LmsApp->getUser()};
|
||||
|
||||
user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField)));
|
||||
|
||||
auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))};
|
||||
if (transcodeBitrateRow)
|
||||
LmsApp->getUser().modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow));
|
||||
user.modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow));
|
||||
|
||||
auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))};
|
||||
if (transcodeFormatRow)
|
||||
LmsApp->getUser().modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow));
|
||||
user.modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow));
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
Session::getPasswordService().updatePassword(LmsApp->getAuthUser(), valueText(PasswordField));
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
}
|
||||
|
||||
void loadData()
|
||||
@@ -115,11 +125,8 @@ class SettingsModel : public Wt::WFormModel
|
||||
{
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
auto res = Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), LmsApp->getUserIdentity(), "");
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
#include <Wt/WFormModel.h>
|
||||
#include <Wt/WLineEdit.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
#include <Wt/Auth/Identity.h>
|
||||
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
@@ -54,6 +55,8 @@ class InitWizardModel : public Wt::WFormModel
|
||||
|
||||
void saveData()
|
||||
{
|
||||
const Database::User::PasswordHash passwordHash {getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8())};
|
||||
|
||||
auto transaction(LmsApp->getDbSession().createUniqueTransaction());
|
||||
|
||||
// Check if a user already exist
|
||||
@@ -61,7 +64,7 @@ class InitWizardModel : public Wt::WFormModel
|
||||
if (!Database::User::getAll(LmsApp->getDbSession()).empty())
|
||||
throw LmsException("Admin user already created");
|
||||
|
||||
Database::User::pointer user {LmsApp->getDbSession().createUser(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())};
|
||||
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8(), passwordHash)};
|
||||
user.modify()->setType(Database::User::Type::ADMIN);
|
||||
}
|
||||
|
||||
@@ -74,11 +77,8 @@ class InitWizardModel : public Wt::WFormModel
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
auto res = Database::Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField),
|
||||
valueText(AdminLoginField), "");
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
@@ -126,7 +126,7 @@ InitWizardView::InitWizardView()
|
||||
setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit));
|
||||
|
||||
Wt::WPushButton* saveButton = bindNew<Wt::WPushButton>("create-btn", Wt::WString::tr("Lms.create"));
|
||||
saveButton->clicked().connect(std::bind([=]
|
||||
saveButton->clicked().connect([=]
|
||||
{
|
||||
updateModel(model.get());
|
||||
|
||||
@@ -138,7 +138,7 @@ InitWizardView::InitWizardView()
|
||||
}
|
||||
|
||||
updateView(model.get());
|
||||
}));
|
||||
});
|
||||
|
||||
updateView(model.get());
|
||||
}
|
||||
|
||||
+26
-18
@@ -28,7 +28,9 @@
|
||||
|
||||
#include <Wt/WFormModel.h>
|
||||
|
||||
#include "auth/AuthService.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "main/Service.hpp"
|
||||
#include "utils/Config.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -78,6 +80,10 @@ class UserModel : public Wt::WFormModel
|
||||
|
||||
void saveData()
|
||||
{
|
||||
boost::optional<Database::User::PasswordHash> passwordHash;
|
||||
if (!valueText(PasswordField).empty())
|
||||
passwordHash = getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
if (_userId)
|
||||
@@ -86,8 +92,11 @@ class UserModel : public Wt::WFormModel
|
||||
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
|
||||
|
||||
// Account
|
||||
if (!valueText(PasswordField).empty())
|
||||
LmsApp->getDbSession().updateUserPassword(user, valueText(PasswordField).toUTF8());
|
||||
if (passwordHash)
|
||||
{
|
||||
user.modify()->setPasswordHash(*passwordHash);
|
||||
user.modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))};
|
||||
if (transcodeBitrateLimitRow)
|
||||
@@ -96,7 +105,7 @@ class UserModel : public Wt::WFormModel
|
||||
else
|
||||
{
|
||||
// Create user
|
||||
Database::User::pointer user = LmsApp->getDbSession().createUser(valueText(LoginField).toUTF8(), valueText(PasswordField).toUTF8());
|
||||
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8(), *passwordHash)};
|
||||
|
||||
auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))};
|
||||
if (transcodeBitrateLimitRow )
|
||||
@@ -125,17 +134,17 @@ class UserModel : public Wt::WFormModel
|
||||
setValue(AudioTranscodeBitrateLimitField, _bitrateModel->getString(*transcodeBitrateLimitRow));
|
||||
}
|
||||
|
||||
Wt::WString getLoginName() const
|
||||
std::string getLoginName() const
|
||||
{
|
||||
if (_userId)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
|
||||
return LmsApp->getDbSession().getUserLoginName(user);
|
||||
return user->getLoginName();
|
||||
}
|
||||
else
|
||||
return valueText(LoginField);
|
||||
return valueText(LoginField).toUTF8();
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
@@ -144,7 +153,9 @@ class UserModel : public Wt::WFormModel
|
||||
|
||||
if (field == LoginField)
|
||||
{
|
||||
const Database::User::pointer user {LmsApp->getDbSession().getUser(valueText(LoginField).toUTF8())};
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())};
|
||||
if (user)
|
||||
error = Wt::WString::tr("Lms.Admin.User.user-already-exists");
|
||||
}
|
||||
@@ -154,17 +165,15 @@ class UserModel : public Wt::WFormModel
|
||||
{
|
||||
if (Wt::asNumber(value(DemoField)))
|
||||
{
|
||||
//Demo account: password must be the same as the login name
|
||||
// Demo account: password must be the same as the login name
|
||||
if (valueText(PasswordField) != getLoginName())
|
||||
error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Evaluate the strength of the password for non demo accounts
|
||||
auto res = Database::Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), getLoginName(), "");
|
||||
|
||||
if (!res.isValid())
|
||||
error = res.message();
|
||||
if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,13 +240,12 @@ UserView::refreshView()
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *userId)};
|
||||
const std::string loginName {LmsApp->getDbSession().getUserLoginName(user)};
|
||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(loginName), Wt::TextFormat::Plain);
|
||||
t->setCondition("if-has-last-login-attempt", true);
|
||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName()), Wt::TextFormat::Plain);
|
||||
t->setCondition("if-has-last-login", true);
|
||||
|
||||
Wt::WLineEdit *lastLoginAttempt {t->bindNew<Wt::WLineEdit>("last-login-attempt")};
|
||||
lastLoginAttempt->setText(LmsApp->getDbSession().getUserLastLoginAttempt(user).toString());
|
||||
lastLoginAttempt->setEnabled(false);
|
||||
Wt::WLineEdit *lastLogin {t->bindNew<Wt::WLineEdit>("last-login")};
|
||||
lastLogin->setText(user->getLastLogin().toString());
|
||||
lastLogin->setEnabled(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ UsersView::refreshView()
|
||||
|
||||
Wt::WTemplate* entry {_container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry"))};
|
||||
|
||||
entry->bindString("name", LmsApp->getDbSession().getUserLoginName(user), Wt::TextFormat::Plain);
|
||||
entry->bindString("name", user->getLoginName(), Wt::TextFormat::Plain);
|
||||
|
||||
// Create tag
|
||||
if (user->isAdmin() || user->isDemo())
|
||||
@@ -104,7 +104,7 @@ UsersView::refreshView()
|
||||
|
||||
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)};
|
||||
if (user)
|
||||
LmsApp->getDbSession().removeUser(user);
|
||||
user.remove();
|
||||
|
||||
_container->removeWidget(entry);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "ArtistLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
+31
-18
@@ -99,17 +99,24 @@ Filters::showDialog()
|
||||
const std::string type {typeCombo->valueText().toUTF8()};
|
||||
const std::string value {valueCombo->valueText().toUTF8()};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
// TODO use a model to store the cluster.id() values
|
||||
Database::IdType clusterId {};
|
||||
|
||||
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)};
|
||||
if (!clusterType)
|
||||
return;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Cluster::pointer cluster {clusterType->getCluster(value)};
|
||||
if (!cluster)
|
||||
return;
|
||||
Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)};
|
||||
if (!clusterType)
|
||||
return;
|
||||
|
||||
add(cluster.id());
|
||||
Database::Cluster::pointer cluster {clusterType->getCluster(value)};
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
clusterId = cluster.id();
|
||||
}
|
||||
|
||||
add(clusterId);
|
||||
});
|
||||
|
||||
dialog->show();
|
||||
@@ -118,23 +125,29 @@ Filters::showDialog()
|
||||
void
|
||||
Filters::add(Database::IdType clusterId)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)};
|
||||
if (!cluster)
|
||||
return;
|
||||
Wt::WTemplate* filter {};
|
||||
|
||||
auto res {_filterIds.insert(clusterId)};
|
||||
if (!res.second)
|
||||
return;
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
auto filter {_filters->addWidget(LmsApp->createCluster(cluster, true))};
|
||||
filter->clicked().connect(std::bind([=]
|
||||
Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)};
|
||||
if (!cluster)
|
||||
return;
|
||||
|
||||
auto res {_filterIds.insert(clusterId)};
|
||||
if (!res.second)
|
||||
return;
|
||||
|
||||
filter = _filters->addWidget(LmsApp->createCluster(cluster, true));
|
||||
}
|
||||
|
||||
filter->clicked().connect([=]
|
||||
{
|
||||
_filters->removeWidget(filter);
|
||||
_filterIds.erase(clusterId);
|
||||
_sigUpdated.emit();
|
||||
}));
|
||||
});
|
||||
|
||||
LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds {2});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "resource/ImageResource.hpp"
|
||||
#include "ReleaseLink.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ std::string getModuleName(Module mod)
|
||||
switch (mod)
|
||||
{
|
||||
case Module::API_SUBSONIC: return "API_SUBSONIC";
|
||||
case Module::AUTH: return "AUTH";
|
||||
case Module::AV: return "AV";
|
||||
case Module::COVER: return "COVER";
|
||||
case Module::DB: return "DB";
|
||||
|
||||
@@ -36,6 +36,7 @@ enum class Severity
|
||||
enum class Module
|
||||
{
|
||||
API_SUBSONIC,
|
||||
AUTH,
|
||||
AV,
|
||||
COVER,
|
||||
DB,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "NetAddress.hpp"
|
||||
|
||||
namespace std
|
||||
{
|
||||
std::size_t hash<boost::asio::ip::address>::operator()(const boost::asio::ip::address& ipAddr) const
|
||||
{
|
||||
if (ipAddr.is_v4())
|
||||
return ipAddr.to_v4().to_ulong();
|
||||
|
||||
if (ipAddr.is_v6())
|
||||
{
|
||||
const auto& range {ipAddr.to_v6().to_bytes()};
|
||||
std::size_t res {};
|
||||
|
||||
for (auto b : range)
|
||||
res ^= std::hash<char>{}(static_cast<char>(b));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return std::hash<std::string>{}(ipAddr.to_string());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
namespace std
|
||||
{
|
||||
template<> struct hash<boost::asio::ip::address>
|
||||
{
|
||||
std::size_t operator()(const boost::asio::ip::address& ipAddr) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
|
||||
@@ -156,4 +156,3 @@ stringFromHex(const std::string& str)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -114,3 +114,29 @@ shuffleContainer(Container& container)
|
||||
std::shuffle(std::begin(container), std::end(container), randGenerator);
|
||||
}
|
||||
|
||||
template <typename Container>
|
||||
void
|
||||
fillRandom(Container& container)
|
||||
{
|
||||
using value_type = typename Container::value_type;
|
||||
static_assert(std::is_integral<value_type>::value, "Integral required");
|
||||
|
||||
std::random_device rd;
|
||||
std::mt19937_64 randGenerator {rd()};
|
||||
std::uniform_int_distribution<> dist {std::numeric_limits<value_type>::min(), std::numeric_limits<value_type>::max()};
|
||||
|
||||
std::for_each(std::begin(container), std::end(container), [&](auto& elem) { elem = dist(randGenerator); });
|
||||
}
|
||||
|
||||
template <typename Container>
|
||||
typename Container::iterator
|
||||
pickRandom(Container& container)
|
||||
{
|
||||
auto now {std::chrono::system_clock::now()};
|
||||
std::mt19937 randGenerator (std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count());
|
||||
std::uniform_int_distribution<> dist {0, static_cast<int>(container.size())};
|
||||
|
||||
return std::next(std::begin(container), dist(randGenerator ));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user