From 693e953df36e35904c79239636ccad63ddfb93a6 Mon Sep 17 00:00:00 2001 From: DasSkelett Date: Fri, 7 Aug 2020 16:58:06 +0200 Subject: [PATCH 1/3] Fix _restore_game_info for invalid/deleted game ids --- KerbalStuff/blueprints/mods.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/KerbalStuff/blueprints/mods.py b/KerbalStuff/blueprints/mods.py index 4cf22dd7..13772f95 100644 --- a/KerbalStuff/blueprints/mods.py +++ b/KerbalStuff/blueprints/mods.py @@ -52,10 +52,11 @@ def _restore_game_info() -> Optional[Game]: game_id = session.get('gameid') if game_id: - game = Game.query.filter(Game.active == True, Game.id == game_id).one() + game = Game.query.filter(Game.active == True, Game.id == game_id).one_or_none() # Make sure it's fully set in the session cookie. - set_game_info(game) - return game + if game: + set_game_info(game) + return game return None @@ -393,7 +394,7 @@ def export_referrals(mod_id: int, mod_name: str) -> werkzeug.wrappers.Response: @loginrequired @with_session def delete(mod_id: int) -> werkzeug.wrappers.Response: - mod, game = _get_mod_game_info(mod_id) + mod, _ = _get_mod_game_info(mod_id) editable = False if current_user: if current_user.admin: From d94e729ba60c5e729681e0e0ce24e791f93f2b70 Mon Sep 17 00:00:00 2001 From: DasSkelett Date: Fri, 7 Aug 2020 17:01:29 +0200 Subject: [PATCH 2/3] Add cascade delete constraints to several relationships --- KerbalStuff/blueprints/mods.py | 36 +++---- KerbalStuff/objects.py | 80 ++++++++------- .../2023_03_20_12_00_00-7eec82634342.py | 99 +++++++++++++++++++ alembic/versions/alembic.pyi | 5 +- 4 files changed, 159 insertions(+), 61 deletions(-) create mode 100644 alembic/versions/2023_03_20_12_00_00-7eec82634342.py diff --git a/KerbalStuff/blueprints/mods.py b/KerbalStuff/blueprints/mods.py index 13772f95..f07ea023 100644 --- a/KerbalStuff/blueprints/mods.py +++ b/KerbalStuff/blueprints/mods.py @@ -2,15 +2,14 @@ import os import random import re -import sys from datetime import datetime, timedelta from shutil import rmtree -from typing import Any, Dict, Tuple, Optional, Union, List +from typing import Any, Dict, Tuple, Optional, Union import werkzeug.wrappers import user_agents -from flask import Blueprint, render_template, send_file, make_response, url_for, abort, session, \ +from flask import Blueprint, render_template, make_response, url_for, abort, session, \ redirect, request from flask_login import current_user from urllib.parse import urlparse @@ -24,9 +23,8 @@ from ..database import db from ..email import send_autoupdate_notification, send_mod_locked from ..objects import Mod, ModVersion, DownloadEvent, FollowEvent, ReferralEvent, \ - Featured, Media, GameVersion, Game, Following + Featured, GameVersion, Game, Following from ..search import get_mod_score -from ..thumbnail import thumb_path_from_background_path from ..purge import purge_download mods = Blueprint('mods', __name__) @@ -403,21 +401,16 @@ def delete(mod_id: int) -> werkzeug.wrappers.Response: editable = True if not editable: abort(403) - for featured in Featured.query.filter(Featured.mod_id == mod.id).all(): - db.delete(featured) - for media in Media.query.filter(Media.mod_id == mod.id).all(): - db.delete(media) - for referral in ReferralEvent.query.filter(ReferralEvent.mod_id == mod.id).all(): - db.delete(referral) - for version in ModVersion.query.filter(ModVersion.mod_id == mod.id).all(): - db.delete(version) - db.delete(mod) - db.commit() - notify_ckan(mod, 'delete', True) + storage = _cfg('storage') if storage: full_path = os.path.join(storage, mod.base_path()) rmtree(full_path) + + db.delete(mod) + db.commit() + notify_ckan(mod, 'delete', True) + return redirect("/profile/" + current_user.username) @@ -633,18 +626,19 @@ def download(mod_id: int, mod_name: Optional[str], version: Optional[str]) -> Op def delete_version(mod_id: int, version_id: str) -> werkzeug.wrappers.Response: mod, game = _get_mod_game_info(mod_id) check_mod_editable(mod) - version = [v for v in mod.versions if v.id == int(version_id)] + version = ModVersion.query.get(version_id) if len(mod.versions) == 1: abort(400) - if len(version) == 0: + if not version: abort(404) - if version[0].id == mod.default_version_id: + if version.id == mod.default_version_id: + abort(400) + if version.mod != mod: abort(400) purge_download(version[0].download_path) - db.delete(version[0]) - mod.versions = [v for v in mod.versions if v.id != int(version_id)] + db.delete(version) db.commit() return redirect(url_for("mods.mod", _anchor='changelog', mod_id=mod.id, mod_name=mod.name)) diff --git a/KerbalStuff/objects.py b/KerbalStuff/objects.py index 04a4b96e..0797c6eb 100644 --- a/KerbalStuff/objects.py +++ b/KerbalStuff/objects.py @@ -19,10 +19,10 @@ class Following(Base): # type: ignore __tablename__ = 'mod_followers' __table_args__ = (PrimaryKeyConstraint('user_id', 'mod_id', name='pk_mod_followers'), ) - mod_id = Column(Integer, ForeignKey('mod.id'), index=True) - mod = relationship('Mod', back_populates='followings') - user_id = Column(Integer, ForeignKey('user.id'), index=True) - user = relationship('User', back_populates='followings') + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'), index=True) + mod = relationship('Mod', back_populates='followings', passive_deletes=True) + user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE'), index=True) + user = relationship('User', back_populates='followings', passive_deletes=True) send_update = Column(Boolean(), default=True, nullable=False) send_autoupdate = Column(Boolean(), default=True, nullable=False) @@ -37,8 +37,8 @@ def __init__(self, mod: Optional['Mod'] = None, user: Optional['User'] = None, class Featured(Base): # type: ignore __tablename__ = 'featured' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', backref=backref('featured', order_by=id)) + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('featured', passive_deletes=True, order_by=id)) created = Column(DateTime, default=datetime.now, index=True) def __repr__(self) -> str: @@ -85,7 +85,7 @@ class User(Base): # type: ignore bgOffsetX = Column(Integer, default=0) bgOffsetY = Column(Integer, default=0) # List of Following objects - followings = relationship('Following', back_populates='user') + followings = relationship('Following', back_populates='user', passive_deletes=True) # List of mods the user follows following = association_proxy('followings', 'mod') dark_theme = Column(Boolean, default=False) @@ -178,8 +178,8 @@ class Game(Base): # type: ignore rating = Column(Float()) releasedate = Column(DateTime) short = Column(Unicode(1024)) - publisher_id = Column(Integer, ForeignKey('publisher.id')) - publisher = relationship('Publisher', backref='games') + publisher_id = Column(Integer, ForeignKey('publisher.id', ondelete='CASCADE')) + publisher = relationship('Publisher', backref=backref('games', passive_deletes=True)) description = Column(Unicode(100000)) short_description = Column(Unicode(1000)) created = Column(DateTime, default=datetime.now, index=True) @@ -226,10 +226,11 @@ class Mod(Base): # type: ignore id = Column(Integer, primary_key=True) created = Column(DateTime, default=datetime.now, index=True) updated = Column(DateTime, default=datetime.now, index=True) - user_id = Column(Integer, ForeignKey('user.id')) - user = relationship('User', backref=backref('mods', order_by=created), foreign_keys=user_id) - game_id = Column(Integer, ForeignKey('game.id')) - game = relationship('Game', backref='mods') + user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE')) + user = relationship('User', backref=backref('mods', passive_deletes=True, order_by=created), + foreign_keys=user_id) + game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE')) + game = relationship('Game', backref=backref('mods', passive_deletes=True)) name = Column(String(100), index=True) description = Column(Unicode(100000)) short_description = Column(Unicode(1000)) @@ -258,7 +259,7 @@ class Mod(Base): # type: ignore download_count = Column(Integer, nullable=False, default=0) ckan = Column(Boolean) # List of Following objects - followings = relationship('Following', back_populates='mod') + followings = relationship('Following', back_populates='mod', passive_deletes=True) # List of users that follow this mods followers = association_proxy('followings', 'user') @@ -287,10 +288,10 @@ class ModList(Base): # type: ignore __tablename__ = 'modlist' id = Column(Integer, primary_key=True) created = Column(DateTime, default=datetime.now, index=True) - user_id = Column(Integer, ForeignKey('user.id')) - user = relationship('User', backref=backref('packs', order_by=created)) - game_id = Column(Integer, ForeignKey('game.id')) - game = relationship('Game', backref='modlists') + user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE')) + user = relationship('User', backref=backref('packs', passive_deletes=True, order_by=created)) + game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE')) + game = relationship('Game', backref=backref('modlists', passive_deletes=True)) # Don't access background directly, use background_url() instead. background = Column(String(512)) # Don't access thumbnail directly, use background_thumb() instead. @@ -336,10 +337,10 @@ def __repr__(self) -> str: class SharedAuthor(Base): # type: ignore __tablename__ = 'sharedauthor' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', backref='shared_authors') - user_id = Column(Integer, ForeignKey('user.id')) - user = relationship('User', backref='shared_authors') + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('shared_authors', passive_deletes=True)) + user_id = Column(Integer, ForeignKey('user.id', ondelete='CASCADE')) + user = relationship('User', backref=backref('shared_authors', passive_deletes=True)) accepted = Column(Boolean, default=False) def __repr__(self) -> str: @@ -368,9 +369,10 @@ def __repr__(self) -> str: class FollowEvent(Base): # type: ignore __tablename__ = 'followevent' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', - backref=backref('follow_events', order_by="desc(FollowEvent.created)")) + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('follow_events', + passive_deletes=True, + order_by="desc(FollowEvent.created)")) events = Column(Integer) delta = Column(Integer, default=0) created = Column(DateTime, default=datetime.now, index=True) @@ -382,9 +384,10 @@ def __repr__(self) -> str: class ReferralEvent(Base): # type: ignore __tablename__ = 'referralevent' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', - backref=backref('referrals', order_by="desc(ReferralEvent.created)")) + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('referrals', + passive_deletes=True, + order_by="desc(ReferralEvent.created)")) host = Column(String) events = Column(Integer, default=0) created = Column(DateTime, default=datetime.now, index=True) @@ -396,13 +399,16 @@ def __repr__(self) -> str: class ModVersion(Base): # type: ignore __tablename__ = 'modversion' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', - backref=backref('versions', order_by="desc(ModVersion.sort_index)"), + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('versions', + passive_deletes=True, + order_by="desc(ModVersion.sort_index)"), foreign_keys=mod_id) friendly_version = Column(String(64)) - gameversion_id = Column(Integer, ForeignKey('gameversion.id')) - gameversion = relationship('GameVersion', backref=backref('mod_versions', order_by=id)) + gameversion_id = Column(Integer, ForeignKey('gameversion.id', ondelete='CASCADE')) + gameversion = relationship('GameVersion', backref=backref('mod_versions', + passive_deletes=True, + order_by=id)) created = Column(DateTime, default=datetime.now) download_path = Column(String(512)) changelog = Column(Unicode(10000)) @@ -436,8 +442,8 @@ def __repr__(self) -> str: class Media(Base): # type: ignore __tablename__ = 'media' id = Column(Integer, primary_key=True) - mod_id = Column(Integer, ForeignKey('mod.id')) - mod = relationship('Mod', backref=backref('media', order_by=id)) + mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE')) + mod = relationship('Mod', backref=backref('media', passive_deletes=True, order_by=id)) hash = Column(String(12)) type = Column(String(32)) data = Column(String(512)) @@ -450,8 +456,8 @@ class GameVersion(Base): # type: ignore __tablename__ = 'gameversion' id = Column(Integer, primary_key=True) friendly_version = Column(String(128)) - game_id = Column(Integer, ForeignKey('game.id')) - game = relationship('Game', backref='versions') + game_id = Column(Integer, ForeignKey('game.id', ondelete='CASCADE')) + game = relationship('Game', backref=backref('versions', passive_deletes=True)) def __repr__(self) -> str: return '' % self.friendly_version diff --git a/alembic/versions/2023_03_20_12_00_00-7eec82634342.py b/alembic/versions/2023_03_20_12_00_00-7eec82634342.py new file mode 100644 index 00000000..6febbbf1 --- /dev/null +++ b/alembic/versions/2023_03_20_12_00_00-7eec82634342.py @@ -0,0 +1,99 @@ +"""Add ondelete='CASCADE' to foreign keys that need it + +Revision ID: 7eec82634342 +Revises: b9e4c97b74c1 +Create Date: 2023-03-20 12:00:00 + +""" + +# revision identifiers, used by Alembic. +revision = '7eec82634342' +down_revision = 'b9e4c97b74c1' + +from alembic import op + + +def upgrade() -> None: + op.drop_constraint('downloadevent_version_id_fkey', 'downloadevent', type_='foreignkey') + op.create_foreign_key('downloadevent_version_id_fkey', 'downloadevent', 'modversion', ['version_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('downloadevent_mod_id_fkey', 'downloadevent', type_='foreignkey') + op.create_foreign_key('downloadevent_mod_id_fkey', 'downloadevent', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('featured_mod_id_fkey', 'featured', type_='foreignkey') + op.create_foreign_key('featured_mod_id_fkey', 'featured', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('followevent_mod_id_fkey', 'followevent', type_='foreignkey') + op.create_foreign_key('followevent_mod_id_fkey', 'followevent', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('game_publisher_id_fkey', 'game', type_='foreignkey') + op.create_foreign_key('game_publisher_id_fkey', 'game', 'publisher', ['publisher_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('gameversion_game_id_fkey', 'gameversion', type_='foreignkey') + op.create_foreign_key('gameversion_game_id_fkey', 'gameversion', 'game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('media_mod_id_fkey', 'media', type_='foreignkey') + op.create_foreign_key('media_mod_id_fkey', 'media', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('mod_game_id_fkey', 'mod', type_='foreignkey') + op.create_foreign_key('mod_game_id_fkey', 'mod', 'game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('mod_user_id_fkey', 'mod', type_='foreignkey') + op.create_foreign_key('mod_user_id_fkey', 'mod', 'user', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modlist_user_id_fkey', 'modlist', type_='foreignkey') + op.create_foreign_key('modlist_user_id_fkey', 'modlist', 'user', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modlist_game_id_fkey', 'modlist', type_='foreignkey') + op.create_foreign_key('modlist_game_id_fkey', 'modlist', 'game', ['game_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modlistitem_mod_id_fkey', 'modlistitem', type_='foreignkey') + op.create_foreign_key('modlistitem_mod_id_fkey', 'modlistitem', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modlistitem_mod_list_id_fkey', 'modlistitem', type_='foreignkey') + op.create_foreign_key('modlistitem_mod_list_id_fkey', 'modlistitem', 'modlist', ['mod_list_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modversion_gameversion_id_fkey', 'modversion', type_='foreignkey') + op.create_foreign_key('modversion_gameversion_id_fkey', 'modversion', 'gameversion', ['gameversion_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('modversion_mod_id_fkey', 'modversion', type_='foreignkey') + op.create_foreign_key('modversion_mod_id_fkey', 'modversion', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('referralevent_mod_id_fkey', 'referralevent', type_='foreignkey') + op.create_foreign_key('referralevent_mod_id_fkey', 'referralevent', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('sharedauthor_mod_id_fkey', 'sharedauthor', type_='foreignkey') + op.create_foreign_key('sharedauthor_mod_id_fkey', 'sharedauthor', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('sharedauthor_user_id_fkey', 'sharedauthor', type_='foreignkey') + op.create_foreign_key('sharedauthor_user_id_fkey', 'sharedauthor', 'user', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('mod_followers_user_id_fkey', 'mod_followers', type_='foreignkey') + op.create_foreign_key('mod_followers_user_id_fkey', 'mod_followers', 'user', ['user_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('mod_followers_mod_id_fkey', 'mod_followers', type_='foreignkey') + op.create_foreign_key('mod_followers_mod_id_fkey', 'mod_followers', 'mod', ['mod_id'], ['id'], ondelete='CASCADE') + + +def downgrade() -> None: + op.drop_constraint('downloadevent_version_id_fkey', 'downloadevent', type_='foreignkey') + op.create_foreign_key('downloadevent_version_id_fkey', 'downloadevent', 'modversion', ['version_id'], ['id']) + op.drop_constraint('downloadevent_mod_id_fkey', 'downloadevent', type_='foreignkey') + op.create_foreign_key('downloadevent_mod_id_fkey', 'downloadevent', 'mod', ['mod_id'], ['id']) + op.drop_constraint('featured_mod_id_fkey', 'featured', type_='foreignkey') + op.create_foreign_key('featured_mod_id_fkey', 'featured', 'mod', ['mod_id'], ['id']) + op.drop_constraint('followevent_mod_id_fkey', 'followevent', type_='foreignkey') + op.create_foreign_key('followevent_mod_id_fkey', 'followevent', 'mod', ['mod_id'], ['id']) + op.drop_constraint('game_publisher_id_fkey', 'game', type_='foreignkey') + op.create_foreign_key('game_publisher_id_fkey', 'game', 'publisher', ['publisher_id'], ['id']) + op.drop_constraint('gameversion_game_id_fkey', 'gameversion', type_='foreignkey') + op.create_foreign_key('gameversion_game_id_fkey', 'gameversion', 'game', ['game_id'], ['id']) + op.drop_constraint('media_mod_id_fkey', 'media', type_='foreignkey') + op.create_foreign_key('media_mod_id_fkey', 'media', 'mod', ['mod_id'], ['id']) + op.drop_constraint('mod_game_id_fkey', 'mod', type_='foreignkey') + op.create_foreign_key('mod_game_id_fkey', 'mod', 'game', ['game_id'], ['id']) + op.drop_constraint('mod_user_id_fkey', 'mod', type_='foreignkey') + op.create_foreign_key('mod_user_id_fkey', 'mod', 'user', ['user_id'], ['id']) + op.drop_constraint('modlist_user_id_fkey', 'modlist', type_='foreignkey') + op.create_foreign_key('modlist_user_id_fkey', 'modlist', 'user', ['user_id'], ['id']) + op.drop_constraint('modlist_game_id_fkey', 'modlist', type_='foreignkey') + op.create_foreign_key('modlist_game_id_fkey', 'modlist', 'game', ['game_id'], ['id']) + op.drop_constraint('modlistitem_mod_id_fkey', 'modlistitem', type_='foreignkey') + op.create_foreign_key('modlistitem_mod_id_fkey', 'modlistitem', 'mod', ['mod_id'], ['id']) + op.drop_constraint('modlistitem_mod_list_id_fkey', 'modlistitem', type_='foreignkey') + op.create_foreign_key('modlistitem_mod_list_id_fkey', 'modlistitem', 'modlist', ['mod_list_id'], ['id']) + op.drop_constraint('modversion_gameversion_id_fkey', 'modversion', type_='foreignkey') + op.create_foreign_key('modversion_gameversion_id_fkey', 'modversion', 'gameversion', ['gameversion_id'], ['id']) + op.drop_constraint('modversion_mod_id_fkey', 'modversion', type_='foreignkey') + op.create_foreign_key('modversion_mod_id_fkey', 'modversion', 'mod', ['mod_id'], ['id']) + op.drop_constraint('referralevent_mod_id_fkey', 'referralevent', type_='foreignkey') + op.create_foreign_key('referralevent_mod_id_fkey', 'referralevent', 'mod', ['mod_id'], ['id']) + op.drop_constraint('sharedauthor_mod_id_fkey', 'sharedauthor', type_='foreignkey') + op.create_foreign_key('sharedauthor_mod_id_fkey', 'sharedauthor', 'mod', ['mod_id'], ['id']) + op.drop_constraint('sharedauthor_user_id_fkey', 'sharedauthor', type_='foreignkey') + op.create_foreign_key('sharedauthor_user_id_fkey', 'sharedauthor', 'user', ['user_id'], ['id']) + op.drop_constraint('mod_followers_user_id_fkey', 'mod_followers', type_='foreignkey') + op.create_foreign_key('mod_followers_user_id_fkey', 'mod_followers', 'user', ['user_id'], ['id']) + op.drop_constraint('mod_followers_mod_id_fkey', 'mod_followers', type_='foreignkey') + op.create_foreign_key('mod_followers_mod_id_fkey', 'mod_followers', 'mod', ['mod_id'], ['id']) diff --git a/alembic/versions/alembic.pyi b/alembic/versions/alembic.pyi index 0cff2282..09cab0cf 100644 --- a/alembic/versions/alembic.pyi +++ b/alembic/versions/alembic.pyi @@ -8,10 +8,9 @@ - https://github.com/miguelgrinberg/Flask-Migrate/issues/155 """ -from typing import List, Optional, Union - -import sqlalchemy.sql.type_api import sqlalchemy as sa +import sqlalchemy.sql.type_api +from typing import List, Optional, Union class op: From 309896a3a4bef14010b4684d43dfe9fa8db55061 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 20 Mar 2023 02:43:53 +0000 Subject: [PATCH 3/3] Add option for users to delete their accounts And one for admins --- KerbalStuff/blueprints/api.py | 37 +++++++++++++++++++++- KerbalStuff/blueprints/mods.py | 8 +++-- frontend/coffee/profile.coffee | 58 ++++++++++++++++++++++++++++++++-- templates/profile.html | 42 ++++++++++++++++++++++-- templates/view_profile.html | 36 +++++++++++++++++++++ 5 files changed, 173 insertions(+), 8 deletions(-) diff --git a/KerbalStuff/blueprints/api.py b/KerbalStuff/blueprints/api.py index e31d13da..a73c1c76 100644 --- a/KerbalStuff/blueprints/api.py +++ b/KerbalStuff/blueprints/api.py @@ -5,10 +5,11 @@ import zipfile from datetime import datetime from functools import wraps +from shutil import rmtree from typing import Dict, Any, Callable, Optional, Tuple, Iterable, List, Union from flask import Blueprint, url_for, current_app, request, abort -from flask_login import login_user, current_user +from flask_login import login_user, current_user, logout_user from sqlalchemy import desc, asc from sqlalchemy.orm import Query from werkzeug.utils import secure_filename @@ -551,6 +552,40 @@ def change_password(username: str) -> Union[Dict[str, Any], Tuple[Union[str, Any return {'error': True, 'reason': pw_message} +@api.route("/api/user//delete", methods=['POST']) +@with_session +@user_required +@json_output +def delete(username: str) -> Tuple[Dict[str, Any], int]: + deletable = False + if current_user: + if current_user.admin: + deletable = True + if current_user.username == username: + deletable = True + if not deletable: + return {'error': True, 'reason': 'Unauthorized'}, 401 + + form_username = request.form.get('username') + if form_username != username: + return {'error': True, 'reason': 'Wrong username'}, 403 + + user = User.query.filter(User.username == username).one_or_none() + if not user: + return {'error': True, 'reason': 'User does not exist'}, 404 + + storage = _cfg('storage') + if storage: + full_path = os.path.join(storage, user.base_path()) + rmtree(full_path, ignore_errors=True) + + db.delete(user) + if user == current_user: + logout_user() + + return {"error": False}, 400 + + @api.route('/api/mod//update-bg', methods=['POST']) @with_session @json_output diff --git a/KerbalStuff/blueprints/mods.py b/KerbalStuff/blueprints/mods.py index f07ea023..3791b1a7 100644 --- a/KerbalStuff/blueprints/mods.py +++ b/KerbalStuff/blueprints/mods.py @@ -405,8 +405,7 @@ def delete(mod_id: int) -> werkzeug.wrappers.Response: storage = _cfg('storage') if storage: full_path = os.path.join(storage, mod.base_path()) - rmtree(full_path) - + rmtree(full_path, ignore_errors=True) db.delete(mod) db.commit() notify_ckan(mod, 'delete', True) @@ -638,6 +637,11 @@ def delete_version(mod_id: int, version_id: str) -> werkzeug.wrappers.Response: purge_download(version[0].download_path) + storage = _cfg('storage') + if storage: + full_path = os.path.join(storage, version.download_path) + os.remove(full_path) + db.delete(version) db.commit() return redirect(url_for("mods.mod", _anchor='changelog', mod_id=mod.id, mod_name=mod.name)) diff --git a/frontend/coffee/profile.coffee b/frontend/coffee/profile.coffee index 330f059b..900bf878 100644 --- a/frontend/coffee/profile.coffee +++ b/frontend/coffee/profile.coffee @@ -17,7 +17,7 @@ window.upload_bg = (files, box) -> xhr.upload.onprogress = (e) -> if e.lengthComputable progress.style.width = (e.loaded / e.total) * 100 + '%' - xhr.onload = (e) -> + xhr.onload = () -> if xhr.status != 200 p.textContent = 'Please upload JPG or PNG only.' setTimeout(() -> @@ -98,6 +98,60 @@ resetPasswordModalDialog = () -> $('#change-password').on('hidden.bs.modal', resetPasswordModalDialog) +# Handling of the delete-account dialog +$('#delete-account-form').submit((e) -> + e.preventDefault() + + # Disable the buttons until we get an response + buttons = document.getElementsByClassName('btn-account-del') + for button in buttons + button.setAttribute('disabled', '') + + form_username = $('#username').val() + + xhr = new XMLHttpRequest() + xhr.open('POST', "/api/user/#{window.username}/delete") + # Triggered after we get an response from the server. + # It's in the form {'error': bool, 'reason': string) + xhr.onload = () -> + result = JSON.parse(this.responseText) + error_message_display = $('#delete-account-error-message') + + if result.error == true + error_message_display.html(result.reason) + error_message_display.addClass('text-danger') + error_message_display.removeClass('hidden') + # Re-enable the buttons. + for button in buttons + button.removeAttribute('disabled') + else + error_message_display.html('Account deleted successfully.') + error_message_display.removeClass('text-danger') + error_message_display.addClass('text-success') + error_message_display.removeClass('hidden') + # .modal('hide') doesn't work. Let's reload the page instead. + # Delay it a bit to give the user a chance to read the response message. + setTimeout((() -> window.location = '/'), 1000) + + form = new FormData() + form.append('username', form_username) + xhr.send(form) +) + +deleteAccountModalDialog = () -> + $('#delete-account-form').trigger('reset') + error_message_display = $('#delete-account-error-message') + error_message_display.html('') + error_message_display.removeClass('text-danger') + error_message_display.removeClass('text-success') + error_message_display.addClass('hidden') + + buttons = document.getElementsByClassName('btn-account-del') + for button in buttons + button.removeAttribute('disabled') + +$('#delete-account').on('hidden.bs.modal', deleteAccountModalDialog) + $('#check-all-updates' ).on('click', () -> $('[id^=updates-]' ).prop('checked', true)) $('#uncheck-all-updates' ).on('click', () -> $('[id^=updates-]' ).prop('checked', false)) $('#check-all-autoupdates' ).on('click', () -> $('[id^=autoupdates-]').prop('checked', true)) @@ -117,4 +171,4 @@ $('#save-changes').on 'click', () -> $('#overall-error').hide() else $('#overall-error').show() - return allValid + return allValid \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html index 57a39b30..6cb4e46b 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -211,11 +211,21 @@

Change Password

Click to change password +
+
+

Delete User Account

+
+
+
-
+

Connected Accounts

@@ -280,7 +290,7 @@