mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2024-08-14 23:51:41 +00:00
Feature: Implement account deletion (#248)
* Add backend code for user deletion * Add endpoint and delete user test * Delete all user content from db * Delete from playlist_videos table on user deletion * Avoid raw SQL in unsubscribeResponse() * Fix unsubscribeResponse() * Don't delete PlaylistVideos from other users' playlists * Fix pruneUnusedPlaylistVideos() * Remove unused commented-out code * Fix oopsie * Proper type declaration due to false error-reporting by VSCode * Use delete query for better performance * Cleanup and add OneToMany relationship. * Revert unsubscribe logic. Co-authored-by: Kavin <20838718+FireMasterK@users.noreply.github.com>
This commit is contained in:
parent
1bf566b1c7
commit
d3d1ee420b
7 changed files with 124 additions and 26 deletions
|
@ -14,6 +14,7 @@ import io.activej.inject.module.Module;
|
||||||
import io.activej.launchers.http.MultithreadedHttpServerLauncher;
|
import io.activej.launchers.http.MultithreadedHttpServerLauncher;
|
||||||
import me.kavin.piped.consts.Constants;
|
import me.kavin.piped.consts.Constants;
|
||||||
import me.kavin.piped.utils.*;
|
import me.kavin.piped.utils.*;
|
||||||
|
import me.kavin.piped.utils.resp.DeleteUserRequest;
|
||||||
import me.kavin.piped.utils.resp.ErrorResponse;
|
import me.kavin.piped.utils.resp.ErrorResponse;
|
||||||
import me.kavin.piped.utils.resp.LoginRequest;
|
import me.kavin.piped.utils.resp.LoginRequest;
|
||||||
import me.kavin.piped.utils.resp.SubscriptionUpdateRequest;
|
import me.kavin.piped.utils.resp.SubscriptionUpdateRequest;
|
||||||
|
@ -327,6 +328,15 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return getErrorResponse(e, request.getPath());
|
return getErrorResponse(e, request.getPath());
|
||||||
}
|
}
|
||||||
|
})).map(POST, "/user/delete", AsyncServlet.ofBlocking(executor, request -> {
|
||||||
|
try {
|
||||||
|
DeleteUserRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||||
|
DeleteUserRequest.class);
|
||||||
|
return getJsonResponse(ResponseHelper.deleteUserResponse(request.getHeader(AUTHORIZATION), body.password),
|
||||||
|
"private");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return getErrorResponse(e, request.getPath());
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return new CustomServletDecorator(router);
|
return new CustomServletDecorator(router);
|
||||||
|
|
|
@ -16,15 +16,19 @@ public class DatabaseHelper {
|
||||||
public static User getUserFromSession(String session) {
|
public static User getUserFromSession(String session) {
|
||||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
s.setHibernateFlushMode(FlushMode.MANUAL);
|
s.setHibernateFlushMode(FlushMode.MANUAL);
|
||||||
CriteriaBuilder cb = s.getCriteriaBuilder();
|
return getUserFromSession(session, s);
|
||||||
CriteriaQuery<User> cr = cb.createQuery(User.class);
|
|
||||||
Root<User> root = cr.from(User.class);
|
|
||||||
cr.select(root).where(cb.equal(root.get("sessionId"), session));
|
|
||||||
|
|
||||||
return s.createQuery(cr).uniqueResult();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static User getUserFromSession(String session, Session s) {
|
||||||
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
||||||
|
CriteriaQuery<User> cr = cb.createQuery(User.class);
|
||||||
|
Root<User> root = cr.from(User.class);
|
||||||
|
cr.select(root).where(cb.equal(root.get("sessionId"), session));
|
||||||
|
|
||||||
|
return s.createQuery(cr).uniqueResult();
|
||||||
|
}
|
||||||
|
|
||||||
public static User getUserFromSessionWithSubscribed(String session) {
|
public static User getUserFromSessionWithSubscribed(String session) {
|
||||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
s.setHibernateFlushMode(FlushMode.MANUAL);
|
s.setHibernateFlushMode(FlushMode.MANUAL);
|
||||||
|
|
|
@ -622,8 +622,6 @@ public class ResponseHelper {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
|
|
||||||
|
|
||||||
public static byte[] registerResponse(String user, String pass) throws IOException {
|
public static byte[] registerResponse(String user, String pass) throws IOException {
|
||||||
|
|
||||||
if (Constants.DISABLE_REGISTRATION)
|
if (Constants.DISABLE_REGISTRATION)
|
||||||
|
@ -641,9 +639,8 @@ public class ResponseHelper {
|
||||||
cr.select(root).where(cb.equal(root.get("username"), user));
|
cr.select(root).where(cb.equal(root.get("username"), user));
|
||||||
boolean registered = s.createQuery(cr).uniqueResult() != null;
|
boolean registered = s.createQuery(cr).uniqueResult() != null;
|
||||||
|
|
||||||
if (registered) {
|
if (registered)
|
||||||
return Constants.mapper.writeValueAsBytes(new AlreadyRegisteredResponse());
|
return Constants.mapper.writeValueAsBytes(new AlreadyRegisteredResponse());
|
||||||
}
|
|
||||||
|
|
||||||
if (Constants.COMPROMISED_PASSWORD_CHECK) {
|
if (Constants.COMPROMISED_PASSWORD_CHECK) {
|
||||||
String sha1Hash = DigestUtils.sha1Hex(pass).toUpperCase();
|
String sha1Hash = DigestUtils.sha1Hex(pass).toUpperCase();
|
||||||
|
@ -668,8 +665,16 @@ public class ResponseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
|
||||||
|
|
||||||
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
|
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
|
private static boolean hashMatch(String hash, String pass) {
|
||||||
|
return hash.startsWith("$argon2") ?
|
||||||
|
argon2PasswordEncoder.matches(pass, hash) :
|
||||||
|
bcryptPasswordEncoder.matches(pass, hash);
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] loginResponse(String user, String pass)
|
public static byte[] loginResponse(String user, String pass)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
|
@ -688,11 +693,7 @@ public class ResponseHelper {
|
||||||
|
|
||||||
if (dbuser != null) {
|
if (dbuser != null) {
|
||||||
String hash = dbuser.getPassword();
|
String hash = dbuser.getPassword();
|
||||||
if (hash.startsWith("$argon2")) {
|
if (hashMatch(hash, pass)) {
|
||||||
if (argon2PasswordEncoder.matches(pass, hash)) {
|
|
||||||
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
|
||||||
}
|
|
||||||
} else if (bcryptPasswordEncoder.matches(pass, hash)) {
|
|
||||||
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -701,6 +702,37 @@ public class ResponseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] deleteUserResponse(String session, String pass) throws IOException {
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(pass))
|
||||||
|
return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||||
|
|
||||||
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
|
User user = DatabaseHelper.getUserFromSession(session);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||||
|
|
||||||
|
String hash = user.getPassword();
|
||||||
|
|
||||||
|
if (!hashMatch(hash, pass))
|
||||||
|
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||||
|
|
||||||
|
try {
|
||||||
|
s.delete(user);
|
||||||
|
|
||||||
|
s.getTransaction().begin();
|
||||||
|
s.getTransaction().commit();
|
||||||
|
|
||||||
|
Multithreading.runAsync(() -> pruneUnusedPlaylistVideos());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Constants.mapper.writeValueAsBytes(new ErrorResponse(ExceptionUtils.getStackTrace(e), e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Constants.mapper.writeValueAsBytes(new DeleteUserResponse(user.getUsername()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] subscribeResponse(String session, String channelId)
|
public static byte[] subscribeResponse(String session, String channelId)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
|
@ -902,6 +934,7 @@ public class ResponseHelper {
|
||||||
Multithreading.runAsync(() -> {
|
Multithreading.runAsync(() -> {
|
||||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
var channels = DatabaseHelper.getChannelsFromIds(s, Arrays.asList(channelIds));
|
var channels = DatabaseHelper.getChannelsFromIds(s, Arrays.asList(channelIds));
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
for (String channelId : channelIds) {
|
for (String channelId : channelIds) {
|
||||||
for (var channel : channels)
|
for (var channel : channels)
|
||||||
|
@ -1001,6 +1034,8 @@ public class ResponseHelper {
|
||||||
|
|
||||||
s.getTransaction().begin();
|
s.getTransaction().begin();
|
||||||
s.getTransaction().commit();
|
s.getTransaction().commit();
|
||||||
|
|
||||||
|
Multithreading.runAsync(() -> pruneUnusedPlaylistVideos());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
||||||
|
@ -1008,21 +1043,16 @@ public class ResponseHelper {
|
||||||
|
|
||||||
public static byte[] playlistsResponse(String session) throws IOException {
|
public static byte[] playlistsResponse(String session) throws IOException {
|
||||||
|
|
||||||
User user = DatabaseHelper.getUserFromSession(session);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
||||||
|
|
||||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
var cb = s.getCriteriaBuilder();
|
|
||||||
var query = cb.createQuery(me.kavin.piped.utils.obj.db.Playlist.class);
|
User user = DatabaseHelper.getUserFromSession(session, s);
|
||||||
var root = query.from(me.kavin.piped.utils.obj.db.Playlist.class);
|
|
||||||
query.select(root);
|
if (user == null)
|
||||||
query.where(cb.equal(root.get("owner"), user));
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||||
|
|
||||||
var playlists = new ObjectArrayList<>();
|
var playlists = new ObjectArrayList<>();
|
||||||
|
|
||||||
for (var playlist : s.createQuery(query).list()) {
|
for (var playlist : user.getPlaylists()) {
|
||||||
ObjectNode node = Constants.mapper.createObjectNode();
|
ObjectNode node = Constants.mapper.createObjectNode();
|
||||||
node.put("id", String.valueOf(playlist.getPlaylistId()));
|
node.put("id", String.valueOf(playlist.getPlaylistId()));
|
||||||
node.put("name", playlist.getName());
|
node.put("name", playlist.getName());
|
||||||
|
@ -1137,6 +1167,8 @@ public class ResponseHelper {
|
||||||
s.getTransaction().begin();
|
s.getTransaction().begin();
|
||||||
s.getTransaction().commit();
|
s.getTransaction().commit();
|
||||||
|
|
||||||
|
Multithreading.runAsync(() -> pruneUnusedPlaylistVideos());
|
||||||
|
|
||||||
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1189,27 @@ public class ResponseHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void pruneUnusedPlaylistVideos() {
|
||||||
|
|
||||||
|
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||||
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
||||||
|
|
||||||
|
var pvQuery = cb.createCriteriaDelete(PlaylistVideo.class);
|
||||||
|
var pvRoot = pvQuery.from(PlaylistVideo.class);
|
||||||
|
|
||||||
|
var subQuery = pvQuery.subquery(me.kavin.piped.utils.obj.db.Playlist.class);
|
||||||
|
var subRoot = subQuery.from(me.kavin.piped.utils.obj.db.Playlist.class);
|
||||||
|
|
||||||
|
subQuery.select(subRoot.join("videos").get("id"));
|
||||||
|
|
||||||
|
pvQuery.where(cb.not(pvRoot.get("id").in(subQuery)));
|
||||||
|
|
||||||
|
s.getTransaction().begin();
|
||||||
|
s.createQuery(pvQuery).executeUpdate();
|
||||||
|
s.getTransaction().commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void handleNewVideo(StreamInfo info, long time, me.kavin.piped.utils.obj.db.Channel channel, Session s) {
|
private static void handleNewVideo(StreamInfo info, long time, me.kavin.piped.utils.obj.db.Channel channel, Session s) {
|
||||||
|
|
||||||
if (channel == null)
|
if (channel == null)
|
||||||
|
|
|
@ -34,6 +34,9 @@ public class User implements Serializable {
|
||||||
@Column(name = "channel", length = 30)
|
@Column(name = "channel", length = 30)
|
||||||
private Set<String> subscribed_ids;
|
private Set<String> subscribed_ids;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
|
||||||
|
private Set<Playlist> playlists;
|
||||||
|
|
||||||
public User() {
|
public User() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,4 +86,12 @@ public class User implements Serializable {
|
||||||
public void setSubscribed(Set<String> subscribed_ids) {
|
public void setSubscribed(Set<String> subscribed_ids) {
|
||||||
this.subscribed_ids = subscribed_ids;
|
this.subscribed_ids = subscribed_ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Playlist> getPlaylists() {
|
||||||
|
return playlists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylists(Set<Playlist> playlists) {
|
||||||
|
this.playlists = playlists;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package me.kavin.piped.utils.resp;
|
||||||
|
|
||||||
|
public class DeleteUserRequest {
|
||||||
|
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package me.kavin.piped.utils.resp;
|
||||||
|
|
||||||
|
public class DeleteUserResponse {
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
|
||||||
|
public DeleteUserResponse(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,3 +107,6 @@ curl ${CURLOPTS[@]} $HOST/user/playlists/remove -X POST -H "Content-Type: applic
|
||||||
|
|
||||||
# Delete Playlist Test
|
# Delete Playlist Test
|
||||||
curl ${CURLOPTS[@]} $HOST/user/playlists/delete -X POST -H "Content-Type: application/json" -H "Authorization: $AUTH_TOKEN" -d $(jq -n --compact-output --arg playlistId $PLAYLIST_ID '{"playlistId": $playlistId}') || exit 1
|
curl ${CURLOPTS[@]} $HOST/user/playlists/delete -X POST -H "Content-Type: application/json" -H "Authorization: $AUTH_TOKEN" -d $(jq -n --compact-output --arg playlistId $PLAYLIST_ID '{"playlistId": $playlistId}') || exit 1
|
||||||
|
|
||||||
|
# Delete User Test
|
||||||
|
curl ${CURLOPTS[@]} $HOST/user/delete -X POST -H "Content-Type: application/json" -H "Authorization: $AUTH_TOKEN" -d $(jq -n --compact-output --arg password "$PASS" '{"password": $password}') || exit 1
|
||||||
|
|
Loading…
Reference in a new issue