Extracted user management
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
|
||||
add_library(lmssubsonic SHARED
|
||||
impl/entrypoints/Bookmark.cpp
|
||||
impl/entrypoints/Scan.cpp
|
||||
impl/entrypoints/Stream.cpp
|
||||
impl/entrypoints/UserManagement.cpp
|
||||
impl/responses/Album.cpp
|
||||
impl/responses/Artist.cpp
|
||||
impl/responses/Bookmark.cpp
|
||||
impl/responses/Song.cpp
|
||||
impl/responses/User.cpp
|
||||
impl/ProtocolVersion.cpp
|
||||
impl/Bookmark.cpp
|
||||
impl/Scan.cpp
|
||||
impl/Stream.cpp
|
||||
impl/ParameterParsing.cpp
|
||||
impl/SubsonicId.cpp
|
||||
impl/SubsonicResource.cpp
|
||||
impl/SubsonicResponse.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmssubsonic INTERFACE
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 "ParameterParsing.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
return parameterMap.find(param) != std::cend(parameterMap);
|
||||
}
|
||||
|
||||
std::string decodePasswordIfNeeded(const std::string& password)
|
||||
{
|
||||
if (password.find("enc:") == 0)
|
||||
{
|
||||
auto decodedPassword{ StringUtils::stringFromHex(password.substr(4)) };
|
||||
if (!decodedPassword)
|
||||
return password; // fallback on plain password
|
||||
|
||||
return *decodedPassword;
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Http/Request.h>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
@@ -28,8 +33,7 @@ namespace API::Subsonic
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
@@ -39,7 +43,7 @@ namespace API::Subsonic
|
||||
|
||||
for (const std::string& param : it->second)
|
||||
{
|
||||
auto value {StringUtils::readAs<T>(param)};
|
||||
auto value{ StringUtils::readAs<T>(param) };
|
||||
if (value)
|
||||
res.emplace_back(std::move(*value));
|
||||
}
|
||||
@@ -48,44 +52,37 @@ namespace API::Subsonic
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
|
||||
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
if (res.empty())
|
||||
throw RequiredParameterMissingError {param};
|
||||
throw RequiredParameterMissingError{ param };
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T>
|
||||
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> params {getMultiParametersAs<T>(parameterMap, param)};
|
||||
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
|
||||
if (params.size() != 1)
|
||||
return std::nullopt;
|
||||
|
||||
return T { std::move(params.front()) };
|
||||
return T{ std::move(params.front()) };
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
auto res {getParameterAs<T>(parameterMap, param)};
|
||||
auto res{ getParameterAs<T>(parameterMap, param) };
|
||||
if (!res)
|
||||
throw RequiredParameterMissingError {param};
|
||||
throw RequiredParameterMissingError{ param };
|
||||
|
||||
return *res;
|
||||
}
|
||||
|
||||
inline
|
||||
bool
|
||||
hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
return parameterMap.find(param) != std::cend(parameterMap);
|
||||
}
|
||||
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
|
||||
std::string decodePasswordIfNeeded(const std::string& password);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,17 +47,20 @@
|
||||
#include "utils/String.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "entrypoints/Bookmark.hpp"
|
||||
#include "entrypoints/Scan.hpp"
|
||||
#include "entrypoints/Stream.hpp"
|
||||
#include "entrypoints/UserManagement.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/Song.hpp"
|
||||
#include "Bookmark.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "ProtocolVersion.hpp"
|
||||
#include "RequestContext.hpp"
|
||||
#include "Scan.hpp"
|
||||
#include "Stream.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
|
||||
using namespace Database;
|
||||
|
||||
@@ -75,14 +78,6 @@ createSubsonicResource(Database::Db& db)
|
||||
return std::make_unique<SubsonicResource>(db);
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
checkSetPasswordImplemented()
|
||||
{
|
||||
Auth::IPasswordService* passwordService {Service<Auth::IPasswordService>::get()};
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError {};
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
@@ -91,22 +86,6 @@ makeNameFilesystemCompatible(const std::string& name)
|
||||
return StringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
decodePasswordIfNeeded(const std::string& password)
|
||||
{
|
||||
if (password.find("enc:") == 0)
|
||||
{
|
||||
auto decodedPassword {StringUtils::stringFromHex(password.substr(4))};
|
||||
if (!decodedPassword)
|
||||
return password; // fallback on plain password
|
||||
|
||||
return *decodedPassword;
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
static
|
||||
std::unordered_map<std::string, ProtocolVersion>
|
||||
readConfigProtocolVersions()
|
||||
@@ -161,18 +140,6 @@ std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username)
|
||||
{
|
||||
User::pointer currentUser {User::find(context.dbSession, context.userId)};
|
||||
if (!currentUser)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
if (currentUser->getLoginName() != username && !currentUser->isAdmin())
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
checkUserTypeIsAllowed(RequestContext& context, EnumSet<Database::UserType> allowedUserTypes)
|
||||
@@ -200,33 +167,6 @@ clusterToResponseNode(const Cluster::pointer& cluster)
|
||||
return clusterNode;
|
||||
}
|
||||
|
||||
static
|
||||
Response::Node
|
||||
userToResponseNode(const User::pointer& user)
|
||||
{
|
||||
Response::Node userNode;
|
||||
|
||||
userNode.setAttribute("username", user->getLoginName());
|
||||
userNode.setAttribute("scrobblingEnabled", true);
|
||||
userNode.setAttribute("adminRole", user->isAdmin());
|
||||
userNode.setAttribute("settingsRole", true);
|
||||
userNode.setAttribute("downloadRole", true);
|
||||
userNode.setAttribute("uploadRole", false);
|
||||
userNode.setAttribute("playlistRole", true);
|
||||
userNode.setAttribute("coverArtRole", false);
|
||||
userNode.setAttribute("commentRole", false);
|
||||
userNode.setAttribute("podcastRole", false);
|
||||
userNode.setAttribute("streamRole", true);
|
||||
userNode.setAttribute("jukeboxRole", false);
|
||||
userNode.setAttribute("shareRole", false);
|
||||
|
||||
Response::Node folder;
|
||||
folder.setValue("0");
|
||||
userNode.addArrayChild("folder", std::move(folder));
|
||||
|
||||
return userNode;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handlePingRequest(RequestContext& context)
|
||||
@@ -234,46 +174,6 @@ handlePingRequest(RequestContext& context)
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleChangePassword(RequestContext& context)
|
||||
{
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
|
||||
try
|
||||
{
|
||||
Database::UserId userId;
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user {User::find(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError {};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError {};
|
||||
}
|
||||
catch (const Auth::Exception& authException)
|
||||
{
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleCreatePlaylistRequest(RequestContext& context)
|
||||
@@ -324,57 +224,6 @@ handleCreatePlaylistRequest(RequestContext& context)
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleCreateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
Database::UserId userId;
|
||||
{
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
User::pointer user {User::find(context.dbSession, username)};
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError {};
|
||||
|
||||
user = context.dbSession.create<User>(username);
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
auto removeCreatedUser {[&]()
|
||||
{
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
User::pointer user {User::find(context.dbSession, userId)};
|
||||
if (user)
|
||||
user.remove();
|
||||
}};
|
||||
|
||||
try
|
||||
{
|
||||
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordMustMatchLoginNameGenericError {};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordTooWeakGenericError {};
|
||||
}
|
||||
catch (const Auth::Exception& exception)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleDeletePlaylistRequest(RequestContext& context)
|
||||
@@ -400,27 +249,6 @@ handleDeletePlaylistRequest(RequestContext& context)
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleDeleteUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
User::pointer user {User::find(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
// cannot delete ourself
|
||||
if (user->getId() == context.userId)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
user.remove();
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetLicenseRequest(RequestContext& context)
|
||||
@@ -1201,45 +1029,6 @@ handleGetSongsByGenreRequest(RequestContext& context)
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
const User::pointer user {User::find(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
response.addNode("user", userToResponseNode(user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleGetUsersRequest(RequestContext& context)
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
Response::Node& usersNode {response.createNode("users")};
|
||||
|
||||
const auto userIds {User::find(context.dbSession, User::FindParameters {})};
|
||||
for (const UserId userId : userIds.results)
|
||||
{
|
||||
const User::pointer user {User::find(context.dbSession, userId)};
|
||||
usersNode.addArrayChild("user", userToResponseNode(user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
@@ -1433,49 +1222,6 @@ handleScrobble(RequestContext& context)
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleUpdateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::optional<std::string> password {getParameterAs<std::string>(context.parameters, "password")};
|
||||
|
||||
UserId userId;
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
User::pointer user {User::find(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
if (password)
|
||||
{
|
||||
checkSetPasswordImplemented();
|
||||
|
||||
try
|
||||
{
|
||||
Service<::Auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError {};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError {};
|
||||
}
|
||||
catch (const Auth::Exception&)
|
||||
{
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
static
|
||||
Response
|
||||
handleUpdatePlaylistRequest(RequestContext& context)
|
||||
@@ -1554,7 +1300,7 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/,
|
||||
throw BadParameterGenericError {"id"};
|
||||
|
||||
std::size_t size {getParameterAs<std::size_t>(context.parameters, "size").value_or(1024)};
|
||||
size = Utils::clamp(size, std::size_t {32}, std::size_t {2048});
|
||||
size = ::Utils::clamp(size, std::size_t {32}, std::size_t {2048});
|
||||
|
||||
std::shared_ptr<Image::IEncodedImage> cover;
|
||||
if (trackId)
|
||||
@@ -1663,10 +1409,10 @@ static const std::unordered_map<std::string_view, RequestEntryPointInfo> request
|
||||
// User management
|
||||
{"/getUser", {handleGetUserRequest}},
|
||||
{"/getUsers", {handleGetUsersRequest, {UserType::ADMIN}}},
|
||||
{"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
{"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
||||
{"/updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}},
|
||||
{"/deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}},
|
||||
{"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &checkSetPasswordImplemented}},
|
||||
{"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &Utils::checkSetPasswordImplemented}},
|
||||
|
||||
// Bookmarks
|
||||
{"/getBookmarks", {handleGetBookmarks}},
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 "Utils.hpp"
|
||||
|
||||
#include "utils/Service.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic::Utils
|
||||
{
|
||||
void checkSetPasswordImplemented()
|
||||
{
|
||||
Auth::IPasswordService* passwordService{ Service<Auth::IPasswordService>::get() };
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace API::Subsonic::Utils
|
||||
{
|
||||
void checkSetPasswordImplemented();
|
||||
}
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#include "UserManagement.hpp"
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "responses/User.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace {
|
||||
void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username)
|
||||
{
|
||||
User::pointer currentUser{ User::find(context.dbSession, context.userId) };
|
||||
if (!currentUser)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
if (currentUser->getLoginName() != username && !currentUser->isAdmin())
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
}
|
||||
|
||||
Response handleGetUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
const User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
response.addNode("user", createUserNode(user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetUsersRequest(RequestContext& context)
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& usersNode{ response.createNode("users") };
|
||||
|
||||
const auto userIds{ User::find(context.dbSession, User::FindParameters {}) };
|
||||
for (const UserId userId : userIds.results)
|
||||
{
|
||||
const User::pointer user{ User::find(context.dbSession, userId) };
|
||||
usersNode.addArrayChild("user", createUserNode(user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleCreateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
Database::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError{};
|
||||
|
||||
user = context.dbSession.create<User>(username);
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
auto removeCreatedUser{ [&]()
|
||||
{
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
User::pointer user {User::find(context.dbSession, userId)};
|
||||
if (user)
|
||||
user.remove();
|
||||
} };
|
||||
|
||||
try
|
||||
{
|
||||
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const Auth::Exception& exception)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleDeleteUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
|
||||
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// cannot delete ourself
|
||||
if (user->getId() == context.userId)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
user.remove();
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleUpdateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
|
||||
|
||||
UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
if (password)
|
||||
{
|
||||
Utils::checkSetPasswordImplemented();
|
||||
|
||||
try
|
||||
{
|
||||
Service<::Auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const Auth::Exception&)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleChangePassword(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
|
||||
try
|
||||
{
|
||||
Database::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const Auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const Auth::Exception& authException)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleGetUserRequest(RequestContext& context);
|
||||
Response handleGetUsersRequest(RequestContext& context);
|
||||
Response handleCreateUserRequest(RequestContext& context);
|
||||
Response handleUpdateUserRequest(RequestContext& context);
|
||||
Response handleDeleteUserRequest(RequestContext& context);
|
||||
Response handleChangePassword(RequestContext& context);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace API::Subsonic
|
||||
}
|
||||
else if (!artists.empty())
|
||||
{
|
||||
albumNode.setAttribute("artist", utils::joinArtistNames(artists));
|
||||
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||
|
||||
if (artists.size() == 1)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace API::Subsonic
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace utils
|
||||
namespace Utils
|
||||
{
|
||||
std::string joinArtistNames(const std::vector<Artist::pointer>& artists)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Database
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
namespace utils
|
||||
namespace Utils
|
||||
{
|
||||
std::string joinArtistNames(const std::vector<Database::ObjectPtr<Database::Artist>>& artists);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace API::Subsonic
|
||||
const std::vector<Artist::pointer>& artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
if (!artists.empty())
|
||||
{
|
||||
trackResponse.setAttribute("artist", utils::joinArtistNames(artists));
|
||||
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||
|
||||
if (artists.size() == 1)
|
||||
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 "responses/User.hpp"
|
||||
|
||||
#include "services/database/User.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Response::Node createUserNode(const User::pointer& user)
|
||||
{
|
||||
Response::Node userNode;
|
||||
|
||||
userNode.setAttribute("username", user->getLoginName());
|
||||
userNode.setAttribute("scrobblingEnabled", true);
|
||||
userNode.setAttribute("adminRole", user->isAdmin());
|
||||
userNode.setAttribute("settingsRole", true);
|
||||
userNode.setAttribute("downloadRole", true);
|
||||
userNode.setAttribute("uploadRole", false);
|
||||
userNode.setAttribute("playlistRole", true);
|
||||
userNode.setAttribute("coverArtRole", false);
|
||||
userNode.setAttribute("commentRole", false);
|
||||
userNode.setAttribute("podcastRole", false);
|
||||
userNode.setAttribute("streamRole", true);
|
||||
userNode.setAttribute("jukeboxRole", false);
|
||||
userNode.setAttribute("shareRole", false);
|
||||
|
||||
Response::Node folder;
|
||||
folder.setValue("0");
|
||||
userNode.addArrayChild("folder", std::move(folder));
|
||||
|
||||
return userNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/database/Object.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response::Node createUserNode(const Database::ObjectPtr<Database::User>& user);
|
||||
}
|
||||
Reference in New Issue
Block a user