ctrl k
  • client/src/pages/account/_index.scss
    ■ ■ ■ ■ ■
    1 1  @use "home";
    2 2  @use "components";
    3 3  @use "notifications";
    4  -@use "keys";
    5 4   
  • client/src/pages/account/keys.html
    ■ ■ ■ ■ ■ ■
    1  -{% extends 'components/shell.html' %}
    2  - 
    3  -{% import 'components/site.html' as site %}
    4  -{% from 'components/forms/base.html' import form %}
    5  -{% from 'components/forms/submit_button.html' import submit_button %}
    6  -{% from 'components/card_list.html' import card_list %}
    7  -{% from 'components/cards/no_results.html' import no_results %}
    8  -{% from 'account/components/service_key.html' import service_key_card %}
    9  - 
    10  -{% set page_title = 'Manage saved keys | Nekohouse' %}
    11  - 
    12  -{% block title %}
    13  - <title>
    14  - {{ page_title }}
    15  - </title>
    16  -{% endblock title %}
    17  - 
    18  -{% block content %}
    19  -{% call site.section('account-keys', 'Stored service keys') %}
    20  - {% call card_list() %}
    21  - {% for key in props.service_keys %}
    22  - <div class="form__section key__view">
    23  - <input
    24  - type="checkbox"
    25  - name="revoke"
    26  - id="key-{{ key.id }}"
    27  - class="form__input key__revoke-check"
    28  - value="{{ key.id }}"
    29  - form="revoke-service-keys"
    30  - >
    31  - <div class="key__info">
    32  - {{ service_key_card(key, import_ids[loop.index0], class_name="key__card") }}
    33  - <label
    34  - for="key-{{ key.id }}"
    35  - class="form__label key__label"
    36  - >Revoke</label>
    37  - </div>
    38  - </div>
    39  - {% else %}
    40  - {{ no_results() }}
    41  - {% endfor %}
    42  - {% endcall %}
    43  - {% if props.service_keys %}
    44  - {% call form(
    45  - id= 'revoke-service-keys',
    46  - action = '/account/keys',
    47  - method= 'POST'
    48  - ) %}
    49  - {{ submit_button('Revoke selected keys') }}
    50  - {% endcall %}
    51  - {% endif %}
    52  -{% endcall %}
    53  -{% endblock content %}
    54  - 
  • client/src/pages/account/keys.scss
    ■ ■ ■ ■ ■ ■
    1  -@use "../../css/variables" as *;
    2  - 
    3  -.site-section--account-keys {
    4  - .key {
    5  - &__view {
    6  - position: relative;
    7  - padding: 0;
    8  - margin-bottom: 3em;
    9  - }
    10  - 
    11  - &__revoke-check {
    12  - position: absolute;
    13  - visibility: hidden;
    14  - opacity: 0;
    15  - max-width: 1px;
    16  - 
    17  - &:checked + .key__info {
    18  - --local-colour1: var(--colour1-tertiary);
    19  - --local-background-colour1: var(--negative-colour1-primary);
    20  - --local-border-colour1: var(--negative-colour1-primary);
    21  - }
    22  - }
    23  - 
    24  - &__info {
    25  - --local-colour1: var(--colour0-primary);
    26  - --local-background-colour1: var(--colour1-tertiary);
    27  - --local-border-colour1: var(--colour1-tertiary);
    28  - 
    29  - max-width: var(--card-size);
    30  - height: 100%;
    31  - border-radius: 10px 10px 0 10px;
    32  - border: $size-thin solid var(--local-border-colour1);
    33  - overflow: hidden;
    34  - transition-duration: var(--duration-global);
    35  - transition-property: border-color;
    36  - 
    37  - & .key__card {
    38  - height: 100%;
    39  - border-radius: 0;
    40  - }
    41  - }
    42  - 
    43  - // &__card {}
    44  - &__label {
    45  - position: absolute;
    46  - top: 100%;
    47  - right: 0;
    48  - color: var(--local-colour1);
    49  - background-color: var(--local-background-colour1);
    50  - border-radius: 0 0 10px 10px;
    51  - border: $size-thin solid var(--local-border-colour1);
    52  - border-top: none;
    53  - padding: $size-small;
    54  - transition-duration: var(--duration-global);
    55  - transition-property: color, background-color, border-color;
    56  - }
    57  - }
    58  -}
    59  - 
  • client/src/pages/components/shell.html
    ■ ■ ■ ■ ■
    skipped 140 lines
    141 141   {% endcall %}
    142 142   {{ nav_list([
    143 143   [
    144  - { 'header': true, 'text': 'Importing', 'icon': '/static/menu/importer.svg' },
    145  - { 'text': 'Import', 'link': '/importer', 'icon': '/static/menu/index.svg' },
     144 + { 'header': true, 'text': 'Import', 'link': '/importer', 'icon': '/static/menu/importer.svg' },
    146 145   { 'text': 'FAQ', 'link': '/importer/tutorial', 'icon': '/static/menu/faq.svg' }
    147 146   ],
    148 147   [
    skipped 24 lines
    173 172   <img src="/static/menu.svg" />
    174 173   </div>
    175 174   {{ header_link('/', 'Home', 'home') }}
    176  - {{ header_link('/importer', 'Importing') }}
     175 + {{ header_link('/importer', 'Import') }}
    177 176   {{ header_link('/artists', 'Artists') }}
    178 177   {{ header_link('/posts', 'Posts') }}
    179 178   {{ header_link('/account/login', 'Login', 'login') }}
    skipped 33 lines
  • client/src/pages/components/shell.js
    ■ ■ ■ ■ ■
    skipped 56 lines
    57 57   login.classList.remove('login');
    58 58   loginHeader.classList.remove('hidden');
    59 59   loginHeader.classList.remove('login');
     60 + register.classList.add('hidden');
    60 61   register.classList.remove('register');
    61 62   favorites.classList.remove('hidden');
    62 63   loginHeader.innerText = 'Favorites';
    63 64   loginHeader.href = '/favorites';
    64  - register.lastChild.textContent = 'Keys';
    65  - register.firstElementChild.src = '/static/menu/keys.svg';
    66  - register.href = '/account/keys';
    67 65   } else {
    68 66   const accountHeader = sidebar.querySelector('.account-header');
    69 67   const newHeader = document.createElement('div');
    skipped 21 lines
  • src/lib/account.py
    ■ ■ ■ ■ ■
    skipped 3 lines
    4 4  from ..lib.words import get_random_words
    5 5  from ..types.account import Service_Key
    6 6  from ..config import Configuration
    7  -from ..internals.cache.redis import (
    8  - get_conn, serialize_dict_list,
    9  - deserialize_dict_list,
    10  - KemonoRedisLock)
     7 +from ..internals.cache.redis import get_conn
    11 8  
    12 9  from datetime import datetime, timedelta, timezone
    13 10  from flask import session, current_app, flash
    skipped 175 lines
    189 186   account = deserialize_account(account)
    190 187  
    191 188   return account
    192  -
    193  -
    194  -def get_saved_key_import_ids(key_id, reload=False):
    195  - redis = get_conn()
    196  - key = 'saved_key_import_ids:' + str(key_id)
    197  - saved_key_import_ids = redis.get(key)
    198  - if saved_key_import_ids is None or reload:
    199  - lock = KemonoRedisLock(redis, key, expire=60, auto_renewal=True)
    200  - if lock.acquire(blocking=False):
    201  - cursor = get_cursor()
    202  - # TODO: select columns
    203  - query = """
    204  - SELECT *
    205  - FROM saved_session_key_import_ids
    206  - WHERE key_id = %s
    207  - """
    208  - cursor.execute(query, (int(key_id),))
    209  - saved_key_import_ids = cursor.fetchall()
    210  - redis.set(key, serialize_dict_list(saved_key_import_ids), ex=3600)
    211  - lock.release()
    212  - else:
    213  - time.sleep(0.1)
    214  - return get_saved_key_import_ids(key_id, reload=reload)
    215  - else:
    216  - saved_key_import_ids = deserialize_dict_list(saved_key_import_ids)
    217  -
    218  - return saved_key_import_ids
    219  -
    220  -
    221  -def get_saved_keys(account_id: int, reload: bool = False):
    222  - redis = get_conn()
    223  - key = 'saved_keys:' + str(account_id)
    224  - saved_keys = redis.get(key)
    225  - result = None
    226  - if saved_keys is None or reload:
    227  - lock = KemonoRedisLock(redis, key, expire=60, auto_renewal=True)
    228  - if lock.acquire(blocking=False):
    229  - cursor = get_cursor()
    230  - args_dict = dict(
    231  - account_id=str(account_id)
    232  - )
    233  - query = """
    234  - SELECT id, service, discord_channel_ids, added, dead
    235  - FROM saved_session_keys_with_hashes
    236  - WHERE contributor_id = %(account_id)s
    237  - ORDER BY
    238  - added DESC
    239  - """
    240  - cursor.execute(query, args_dict)
    241  - result = cursor.fetchall()
    242  - redis.set(key, serialize_dict_list(result), ex=3600)
    243  - lock.release()
    244  - else:
    245  - time.sleep(0.1)
    246  - return get_saved_keys(account_id, reload=reload)
    247  - else:
    248  - result = deserialize_dict_list(saved_keys)
    249  - saved_keys = [Service_Key.init_from_dict(service_key) for service_key in result]
    250  - return saved_keys
    251  -
    252  -
    253  -def revoke_saved_keys(key_ids: List[int], account_id: int):
    254  - cursor = get_cursor()
    255  - query_args = dict(
    256  - key_ids=key_ids,
    257  - account_id=account_id
    258  - )
    259  - query1 = """
    260  - DELETE
    261  - FROM saved_session_key_import_ids skid
    262  - USING saved_session_keys_with_hashes sk
    263  - WHERE
    264  - skid.key_id = sk.id
    265  - AND sk.id = ANY (%(key_ids)s)
    266  - AND sk.contributor_id = %(account_id)s
    267  - """
    268  - cursor.execute(query1, query_args)
    269  - query2 = """
    270  - DELETE
    271  - FROM saved_session_keys_with_hashes
    272  - WHERE
    273  - id = ANY (%(key_ids)s)
    274  - AND contributor_id = %(account_id)s
    275  - """
    276  - cursor.execute(query2, query_args)
    277  - redis = get_conn()
    278  - key = 'saved_keys:' + str(account_id)
    279  - redis.delete(key)
    280  - return True
    281 189  
    282 190  
    283 191  def get_login_info_for_username(username):
    skipped 132 lines
  • src/pages/account/blueprint.py
    ■ ■ ■ ■ ■
    skipped 8 lines
    9 9   flash,
    10 10   g)
    11 11   
    12  -from .types import AccountPageProps, NotificationsProps, ServiceKeysProps
     12 +from .types import AccountPageProps, NotificationsProps
    13 13  from .administrator import administrator
    14 14  from .moderator import moderator
    15 15   
    16 16  from src.lib.notification import count_account_notifications, get_account_notifications, set_notifications_as_seen
    17 17  from src.lib.security import is_password_compromised
    18 18  from src.utils.utils import get_value, set_query_parameter
    19  -from src.types.props import SuccessProps
    20 19  from src.types.account import Account
    21 20  from src.lib.account import (
    22  - get_saved_key_import_ids,
    23  - revoke_saved_keys,
    24 21   is_username_taken,
    25 22   revoke_session,
    26  - get_saved_keys,
    27 23   create_account,
    28 24   attempt_login,
    29 25   load_account,
    skipped 42 lines
    72 68   'account/notifications.html',
    73 69   props=props
    74 70   ), 200)
    75  - 
    76  - 
    77  -@account.get('/account/keys')
    78  -def get_account_keys():
    79  - account: Account = g.get('account')
    80  - if not account:
    81  - return redirect(url_for('account.get_login', logged_in='no'))
    82  - 
    83  - saved_keys = get_saved_keys(account.id)
    84  - props = ServiceKeysProps(
    85  - service_keys=saved_keys
    86  - )
    87  - 
    88  - saved_session_key_import_ids = []
    89  - for key in saved_keys:
    90  - saved_session_key_import_ids.append(get_saved_key_import_ids(key.id))
    91  - 
    92  - response = make_response(render_template(
    93  - 'account/keys.html',
    94  - props=props,
    95  - import_ids=saved_session_key_import_ids
    96  - ), 200)
    97  - response.headers['Cache-Control'] = 's-maxage=60'
    98  - return response
    99  - 
    100  - 
    101  -@account.post('/account/keys')
    102  -def revoke_service_keys():
    103  - account: Account = g.get('account')
    104  - if not account:
    105  - return redirect(url_for('account.get_login', logged_in='no'))
    106  - 
    107  - keys_dict = request.form.to_dict(flat=False)
    108  - keys_for_revocation = [int(key) for key in keys_dict['revoke']] if keys_dict.get('revoke') else []
    109  - 
    110  - revoke_saved_keys(keys_for_revocation, account.id)
    111  - 
    112  - props = SuccessProps(
    113  - currentPage='account',
    114  - redirect='/account/keys'
    115  - )
    116  - 
    117  - response = make_response(render_template(
    118  - 'success.html',
    119  - props=props
    120  - ), 200)
    121  - return response
    122 71   
    123 72   
    124 73  @account.get('/account/login')
    skipped 130 lines
Page is in error, reload to recover