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