diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 25c82747..960ff5d9 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -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 diff --git a/src/libs/subsonic/impl/ParameterParsing.cpp b/src/libs/subsonic/impl/ParameterParsing.cpp new file mode 100644 index 00000000..4e474c03 --- /dev/null +++ b/src/libs/subsonic/impl/ParameterParsing.cpp @@ -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 . + */ + +#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; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/ParameterParsing.hpp b/src/libs/subsonic/impl/ParameterParsing.hpp index 98635c68..414d3aa6 100644 --- a/src/libs/subsonic/impl/ParameterParsing.hpp +++ b/src/libs/subsonic/impl/ParameterParsing.hpp @@ -16,10 +16,15 @@ * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ + #pragma once #include +#include +#include +#include + #include "services/database/Types.hpp" #include "utils/String.hpp" #include "SubsonicResponse.hpp" @@ -28,8 +33,7 @@ namespace API::Subsonic { template - std::vector - getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) + std::vector getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) { std::vector res; @@ -39,7 +43,7 @@ namespace API::Subsonic for (const std::string& param : it->second) { - auto value {StringUtils::readAs(param)}; + auto value{ StringUtils::readAs(param) }; if (value) res.emplace_back(std::move(*value)); } @@ -48,44 +52,37 @@ namespace API::Subsonic } template - std::vector - getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) + std::vector getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) { - std::vector res {getMultiParametersAs(parameterMap, param)}; + std::vector res{ getMultiParametersAs(parameterMap, param) }; if (res.empty()) - throw RequiredParameterMissingError {param}; + throw RequiredParameterMissingError{ param }; return res; } template - std::optional - getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) + std::optional getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) { - std::vector params {getMultiParametersAs(parameterMap, param)}; + std::vector params{ getMultiParametersAs(parameterMap, param) }; if (params.size() != 1) return std::nullopt; - return T { std::move(params.front()) }; + return T{ std::move(params.front()) }; } template - 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(parameterMap, param)}; + auto res{ getParameterAs(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); } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 182820d3..9d7dbbe6 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -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(db); } -static -void -checkSetPasswordImplemented() -{ - Auth::IPasswordService* passwordService {Service::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 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 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(context.parameters, "username")}; - std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(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::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(context.parameters, "username")}; - std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(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(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::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(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(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(context.parameters, "username")}; - std::optional password {getParameterAs(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(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 cover; if (trackId) @@ -1663,10 +1409,10 @@ static const std::unordered_map 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}}, diff --git a/src/libs/subsonic/impl/Utils.cpp b/src/libs/subsonic/impl/Utils.cpp new file mode 100644 index 00000000..3bfb17ab --- /dev/null +++ b/src/libs/subsonic/impl/Utils.cpp @@ -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 . + */ + +#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::get() }; + if (!passwordService || !passwordService->canSetPasswords()) + throw NotImplementedGenericError{}; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/Utils.hpp b/src/libs/subsonic/impl/Utils.hpp new file mode 100644 index 00000000..774711bb --- /dev/null +++ b/src/libs/subsonic/impl/Utils.hpp @@ -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 . + */ + +#pragma once + +namespace API::Subsonic::Utils +{ + void checkSetPasswordImplemented(); +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/Bookmark.cpp b/src/libs/subsonic/impl/entrypoints/Bookmark.cpp similarity index 100% rename from src/libs/subsonic/impl/Bookmark.cpp rename to src/libs/subsonic/impl/entrypoints/Bookmark.cpp diff --git a/src/libs/subsonic/impl/Bookmark.hpp b/src/libs/subsonic/impl/entrypoints/Bookmark.hpp similarity index 98% rename from src/libs/subsonic/impl/Bookmark.hpp rename to src/libs/subsonic/impl/entrypoints/Bookmark.hpp index 14d95742..e1b6d5cd 100644 --- a/src/libs/subsonic/impl/Bookmark.hpp +++ b/src/libs/subsonic/impl/entrypoints/Bookmark.hpp @@ -19,8 +19,6 @@ #pragma once -#include - #include "RequestContext.hpp" #include "SubsonicResponse.hpp" diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/entrypoints/Scan.cpp similarity index 100% rename from src/libs/subsonic/impl/Scan.cpp rename to src/libs/subsonic/impl/entrypoints/Scan.cpp diff --git a/src/libs/subsonic/impl/Scan.hpp b/src/libs/subsonic/impl/entrypoints/Scan.hpp similarity index 100% rename from src/libs/subsonic/impl/Scan.hpp rename to src/libs/subsonic/impl/entrypoints/Scan.hpp diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/entrypoints/Stream.cpp similarity index 100% rename from src/libs/subsonic/impl/Stream.cpp rename to src/libs/subsonic/impl/entrypoints/Stream.cpp diff --git a/src/libs/subsonic/impl/Stream.hpp b/src/libs/subsonic/impl/entrypoints/Stream.hpp similarity index 100% rename from src/libs/subsonic/impl/Stream.hpp rename to src/libs/subsonic/impl/entrypoints/Stream.hpp diff --git a/src/libs/subsonic/impl/entrypoints/UserManagement.cpp b/src/libs/subsonic/impl/entrypoints/UserManagement.cpp new file mode 100644 index 00000000..d154726c --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/UserManagement.cpp @@ -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(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(context.parameters, "username") }; + std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs(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(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::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(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(context.parameters, "username") }; + std::optional password{ getParameterAs(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(context.parameters, "username") }; + std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs(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::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); + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/entrypoints/UserManagement.hpp b/src/libs/subsonic/impl/entrypoints/UserManagement.hpp new file mode 100644 index 00000000..e4fac6f4 --- /dev/null +++ b/src/libs/subsonic/impl/entrypoints/UserManagement.hpp @@ -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 . + */ + +#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); +} diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 7c6697e0..5855d755 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -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) { diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 269a2543..32f05851 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -33,7 +33,7 @@ namespace API::Subsonic using namespace Database; - namespace utils + namespace Utils { std::string joinArtistNames(const std::vector& artists) { diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp index b9614a11..06fd71ec 100644 --- a/src/libs/subsonic/impl/responses/Artist.hpp +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -33,7 +33,7 @@ namespace Database namespace API::Subsonic { - namespace utils + namespace Utils { std::string joinArtistNames(const std::vector>& artists); } diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index ad229397..53ca66cd 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -130,7 +130,7 @@ namespace API::Subsonic const std::vector& 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())); diff --git a/src/libs/subsonic/impl/responses/User.cpp b/src/libs/subsonic/impl/responses/User.cpp new file mode 100644 index 00000000..16f3c532 --- /dev/null +++ b/src/libs/subsonic/impl/responses/User.cpp @@ -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 . + */ + +#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; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/User.hpp b/src/libs/subsonic/impl/responses/User.hpp new file mode 100644 index 00000000..8ce1755f --- /dev/null +++ b/src/libs/subsonic/impl/responses/User.hpp @@ -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 . + */ + +#pragma once + +#include "services/database/Object.hpp" +#include "SubsonicResponse.hpp" + +namespace Database +{ + class User; +} + +namespace API::Subsonic +{ + Response::Node createUserNode(const Database::ObjectPtr& user); +} \ No newline at end of file