diff --git a/onlylegs/api.py b/onlylegs/api.py index 44c187c..0c68f82 100644 --- a/onlylegs/api.py +++ b/onlylegs/api.py @@ -3,7 +3,6 @@ Onlylegs - API endpoints """ import os import pathlib -import re import logging from uuid import uuid4 @@ -23,88 +22,13 @@ from flask_login import login_required, current_user from colorthief import ColorThief from onlylegs.extensions import db -from onlylegs.models import Users, Pictures, Exif +from onlylegs.models import Pictures, Exif from onlylegs.utils.generate_image import generate_thumbnail blueprint = Blueprint("api", __name__, url_prefix="/api") -@blueprint.route("/account/picture/<int:user_id>", methods=["POST"]) -@login_required -def account_picture(user_id): - """ - Returns the profile of a user - """ - user = db.get_or_404(Users, user_id) - file = request.files.get("file", None) - - # If no image is uploaded, return 404 error - if not file: - return jsonify({"error": "No file uploaded"}), 400 - if user.id != current_user.id: - return jsonify({"error": "You are not allowed to do this, go away"}), 403 - - # Get file extension, generate random name and set file path - img_ext = pathlib.Path(file.filename).suffix.replace(".", "").lower() - img_name = str(user.id) - img_path = os.path.join(current_app.config["PFP_FOLDER"], img_name + "." + img_ext) - - # Check if file extension is allowed - if img_ext not in current_app.config["ALLOWED_EXTENSIONS"].keys(): - logging.info("File extension not allowed: %s", img_ext) - return jsonify({"error": "File extension not allowed"}), 403 - - if user.picture: - # Delete cached files and old image - os.remove(os.path.join(current_app.config["PFP_FOLDER"], user.picture)) - cache_name = user.picture.rsplit(".")[0] - for cache_file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob( - cache_name + "*" - ): - os.remove(cache_file) - - # Save file - try: - file.save(img_path) - except OSError as err: - logging.info("Error saving file %s because of %s", img_path, err) - return jsonify({"error": "Error saving file"}), 500 - - img_colors = ColorThief(img_path).get_color() - - # Save to database - user.colour = img_colors - user.picture = str(img_name + "." + img_ext) - db.session.commit() - - return jsonify({"message": "File uploaded"}), 200 - - -@blueprint.route("/account/username/<int:user_id>", methods=["POST"]) -@login_required -def account_username(user_id): - """ - Returns the profile of a user - """ - user = db.get_or_404(Users, user_id) - new_name = request.form["name"] - - username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b") - - # Validate the form - if not new_name or not username_regex.match(new_name): - return jsonify({"error": "Username is invalid"}), 400 - if user.id != current_user.id: - return jsonify({"error": "You are not allowed to do this, go away"}), 403 - - # Save to database - user.username = new_name - db.session.commit() - - return jsonify({"message": "Username changed"}), 200 - - @blueprint.route("/media/<path:path>", methods=["GET"]) def media(path): """ diff --git a/onlylegs/auth.py b/onlylegs/auth.py index c3d0698..4394b31 100644 --- a/onlylegs/auth.py +++ b/onlylegs/auth.py @@ -105,5 +105,5 @@ def logout(): Clear the current session, including the stored user id """ logout_user() - flash(["Goodbye!!!", "4"]) + flash("Goodbye!!!", "4") return redirect(url_for("gallery.index")) diff --git a/onlylegs/config.py b/onlylegs/config.py index 2b792d4..3300411 100644 --- a/onlylegs/config.py +++ b/onlylegs/config.py @@ -44,9 +44,10 @@ WEBSITE_CONF = conf["website"] # Directories UPLOAD_FOLDER = os.path.join(APPLICATION_ROOT, "media", "uploads") +MEDIA_FOLDER = os.path.join(APPLICATION_ROOT, "media") CACHE_FOLDER = os.path.join(APPLICATION_ROOT, "media", "cache") PFP_FOLDER = os.path.join(APPLICATION_ROOT, "media", "pfp") -MEDIA_FOLDER = os.path.join(APPLICATION_ROOT, "media") +BANNER_FOLDER = os.path.join(APPLICATION_ROOT, "media", "banner") # Database INSTANCE_DIR = os.path.join(APPLICATION_ROOT, "instance") diff --git a/onlylegs/filters.py b/onlylegs/filters.py index bdecca5..1f4515b 100644 --- a/onlylegs/filters.py +++ b/onlylegs/filters.py @@ -18,7 +18,11 @@ def colour_contrast(colour): "color: var(--fg-white);" or "color: var(--fg-black);" """ colour_obj = colour_utils.Colour(colour) - return "rgb(var(--fg-black));" if colour_obj.is_light() else "rgb(var(--fg-white));" + return ( + "var(--foreground-black);" + if colour_obj.is_light() + else "var(--foreground-white);" + ) @blueprint.app_template_filter() diff --git a/onlylegs/static/sass/components/banner.sass b/onlylegs/static/sass/components/banner.sass index a88b2c3..724a5ee 100644 --- a/onlylegs/static/sass/components/banner.sass +++ b/onlylegs/static/sass/components/banner.sass @@ -44,7 +44,7 @@ width: 100% height: 100% - background: linear-gradient(to right, var(--background-100), transparent) + background: linear-gradient(to right, var(--background-100), transparent 80%, var(--background-100) 100%) z-index: +1 @@ -157,7 +157,7 @@ margin-left: auto width: auto -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .banner, .banner-small &::after @@ -169,7 +169,7 @@ max-height: 30vh .banner-filter - background: linear-gradient(to bottom, var(--background-100), transparent) + background: linear-gradient(to top, var(--background-100), transparent) .banner-content padding: 0.5rem diff --git a/onlylegs/static/sass/components/buttons/block.sass b/onlylegs/static/sass/components/buttons/block.sass index 121aba2..7994647 100644 --- a/onlylegs/static/sass/components/buttons/block.sass +++ b/onlylegs/static/sass/components/buttons/block.sass @@ -39,35 +39,35 @@ &:hover, &:focus-visible background-color: var(--primary) - color: var(--white) + color: var(--black) &.success background-color: var(--success-transparent) color: var(--success) &:hover, &:focus-visible background-color: var(--success) - color: var(--white) + color: var(--black) &.warning background-color: var(--warning-transparent) color: var(--warning) &:hover, &:focus-visible background-color: var(--warning) - color: var(--white) + color: var(--black) &.critical background-color: var(--danger-transparent) color: var(--danger) &:hover, &:focus-visible background-color: var(--danger) - color: var(--white) + color: var(--black) &.info background-color: var(--info-transparent) color: var(--info) &:hover, &:focus-visible background-color: var(--info) - color: var(--white) + color: var(--black) &.black background-color: var(--black-transparent) color: var(--white) @@ -201,4 +201,4 @@ &.error background-color: var(--danger) - color: var(--danger) + color: var(--black) diff --git a/onlylegs/static/sass/components/buttons/top-of-page.sass b/onlylegs/static/sass/components/buttons/top-of-page.sass index 1ce9cdb..dd356ba 100644 --- a/onlylegs/static/sass/components/buttons/top-of-page.sass +++ b/onlylegs/static/sass/components/buttons/top-of-page.sass @@ -13,7 +13,7 @@ justify-content: center align-items: center - background-color: var(--background-300) + background-color: var(--background-100) color: var(--foreground-white) border-radius: calc(var(--rad) / 2) border: none @@ -34,6 +34,6 @@ right: 0.75rem opacity: 1 -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .top-of-page bottom: 4.25rem diff --git a/onlylegs/static/sass/components/context-menu.sass b/onlylegs/static/sass/components/context-menu.sass index 0a58aed..8d4b4aa 100644 --- a/onlylegs/static/sass/components/context-menu.sass +++ b/onlylegs/static/sass/components/context-menu.sass @@ -31,7 +31,7 @@ overflow: hidden - transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out + transition: transform 0.35s var(--animation-bounce), opacity 0.35s var(--animation-bounce) transform-origin: center center opacity: 0.5 transform: scale(0, 0) diff --git a/onlylegs/static/sass/components/gallery.sass b/onlylegs/static/sass/components/gallery.sass index 7194f63..081af88 100644 --- a/onlylegs/static/sass/components/gallery.sass +++ b/onlylegs/static/sass/components/gallery.sass @@ -26,7 +26,7 @@ box-sizing: border-box overflow: hidden - transition: box-shadow 0.2s cubic-bezier(.79, .14, .15, .86) + transition: box-shadow 0.2s var(--animation-smooth) .image-filter margin: 0 @@ -47,7 +47,7 @@ opacity: 0 // hide z-index: +4 - transition: opacity 0.2s cubic-bezier(.79, .14, .15, .86) + transition: opacity 0.2s var(--animation-smooth) .image-title, .image-subtitle @@ -163,7 +163,7 @@ border-radius: calc(var(--rad) / 2) box-shadow: 0 0 0.4rem 0.25rem var(--black-transparent) - transition: transform 0.2s cubic-bezier(.79, .14, .15, .86) + transition: transform 0.2s var(--animation-smooth) &.size-1 .data-1 @@ -218,5 +218,5 @@ grid-template-columns: auto auto auto .gallery-item - margin: 0.35rem + margin: 0.1rem position: relative \ No newline at end of file diff --git a/onlylegs/static/sass/components/image-view.sass b/onlylegs/static/sass/components/image-view.sass index 1edcd8a..473bdc6 100644 --- a/onlylegs/static/sass/components/image-view.sass +++ b/onlylegs/static/sass/components/image-view.sass @@ -58,13 +58,6 @@ details font-size: 1.1rem font-weight: 500 - &[open] - summary - margin-bottom: 0.5rem - - > i.collapse-indicator - transform: rotate(90deg) - p margin: 0 padding: 0 @@ -137,6 +130,16 @@ details tr:last-of-type td padding-bottom: 0 + &[open] + summary + margin-bottom: 0.5rem + + > i.collapse-indicator + transform: rotate(90deg) + + &:last-of-type + margin-bottom: 0 + .img-colours width: 100% diff --git a/onlylegs/static/sass/components/navigation.sass b/onlylegs/static/sass/components/navigation.sass index a3d8cd9..9ea4332 100644 --- a/onlylegs/static/sass/components/navigation.sass +++ b/onlylegs/static/sass/components/navigation.sass @@ -78,7 +78,7 @@ nav background-color: currentColor border-radius: calc(var(--rad) / 2) -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) nav width: 100vw height: 3.5rem diff --git a/onlylegs/static/sass/components/notification.sass b/onlylegs/static/sass/components/notification.sass index d2c74b0..d7a3de5 100644 --- a/onlylegs/static/sass/components/notification.sass +++ b/onlylegs/static/sass/components/notification.sass @@ -32,7 +32,7 @@ position: relative - background-color: var(--background-300) + background-color: var(--background-400) border-radius: calc(var(--rad) / 2) color: var(--foreground-white) opacity: 0 @@ -97,7 +97,7 @@ justify-content: center align-items: center - background-color: var(--background-200) + background-color: var(--background-300) i font-size: 1.25rem @@ -119,7 +119,7 @@ line-height: 1 text-align: left -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .notifications bottom: 3.8rem width: calc(100vw - 0.6rem) diff --git a/onlylegs/static/sass/components/pop-up.sass b/onlylegs/static/sass/components/pop-up.sass index 565f63b..9542f13 100644 --- a/onlylegs/static/sass/components/pop-up.sass +++ b/onlylegs/static/sass/components/pop-up.sass @@ -38,12 +38,12 @@ display: flex flex-direction: column - background-color: var(--background-200) + background-color: var(--background-400) border-radius: var(--rad) overflow: hidden z-index: +2 - transition: transform 0.2s var(--animation-smooth) + transition: transform 0.2s var(--animation-bounce) .pop-up-header margin: 0 0 0.5rem 0 @@ -133,7 +133,7 @@ .pop-up-wrapper transform: translate(-50%, 50%) scale(1) -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .pop-up .pop-up-wrapper max-width: calc(100% - 0.75rem) diff --git a/onlylegs/static/sass/components/tags.sass b/onlylegs/static/sass/components/tags.sass index 346918a..b45b630 100644 --- a/onlylegs/static/sass/components/tags.sass +++ b/onlylegs/static/sass/components/tags.sass @@ -13,7 +13,7 @@ border-radius: calc(var(--rad) / 2) border: none - background-color: var(--primary--transparent) + background-color: var(--primary-transparent) color: var(--primary) cursor: pointer @@ -23,4 +23,4 @@ font-size: 1.15rem &:hover - background-color: var(--primary--transparent) + background-color: var(--primary-transparent) diff --git a/onlylegs/static/sass/components/upload-panel.sass b/onlylegs/static/sass/components/upload-panel.sass index 04bb941..e4fb816 100644 --- a/onlylegs/static/sass/components/upload-panel.sass +++ b/onlylegs/static/sass/components/upload-panel.sass @@ -13,7 +13,7 @@ overflow: hidden z-index: 68 - transition: background-color 0.25s cubic-bezier(0.76, 0, 0.17, 1) + transition: background-color 0.25s var(--animation-smooth) h3 margin: 0 @@ -70,7 +70,7 @@ z-index: +2 - transition: left 0.25s cubic-bezier(0.76, 0, 0.17, 1), bottom 0.25s cubic-bezier(0.76, 0, 0.17, 1) + transition: left 0.25s var(--animation-smooth), bottom 0.25s var(--animation-smooth) #dragIndicator display: none @@ -97,7 +97,7 @@ background-color: var(--background-400) border-radius: calc(var(--rad) / 2) - transition: width 0.25s var(--animation-bounce) + transition: width 0.25s var(--animation-smooth) &.dragging #dragIndicator::after width: 9rem @@ -163,7 +163,7 @@ z-index: +3 - transition: color 0.25s cubic-bezier(0.76, 0, 0.17, 1) + transition: color 0.25s var(--animation-smooth) .progress width: 100% @@ -175,10 +175,10 @@ background-color: var(--primary) - animation: uploadingLoop 1s cubic-bezier(0.76, 0, 0.17, 1) infinite + animation: uploadingLoop 1s var(--animation-smooth) infinite z-index: +5 - transition: left 1s cubic-bezier(0.76, 0, 0.17, 1) + transition: left 1s var(--animation-smooth) &.critical .job__status, .progress @@ -203,7 +203,7 @@ .container left: 0 -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .upload-panel width: 100% height: calc(100vh - 3.5rem) diff --git a/onlylegs/static/sass/style.sass b/onlylegs/static/sass/style.sass index e607c97..19a6ec1 100644 --- a/onlylegs/static/sass/style.sass +++ b/onlylegs/static/sass/style.sass @@ -57,7 +57,7 @@ body color: var(--foreground-white) overflow-x: hidden -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) body padding: 0 0 3.5rem 0 @@ -71,10 +71,13 @@ main color: var(--foreground-black) border-radius: var(--rad) overflow: hidden -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) main margin: 0 0.5rem - // border-radius: 0 + +header + position: sticky + top: 0 .error-page min-height: 100% @@ -100,7 +103,7 @@ main font-size: 1.25rem font-weight: 400 text-align: center -@media (max-width: var(--breakpoint)) +@media (max-width: 800px) .error-page h1 font-size: 4.5rem diff --git a/onlylegs/static/sass/variables.sass b/onlylegs/static/sass/variables.sass index 754caa2..8f5818f 100644 --- a/onlylegs/static/sass/variables.sass +++ b/onlylegs/static/sass/variables.sass @@ -1,6 +1,6 @@ \:root - --background-hsl-hue: 0 - --background-hsl-saturation: 2% + --background-hsl-hue: 69 + --background-hsl-saturation: 25% --background-100: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%) --background-200: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 8%) @@ -13,14 +13,15 @@ --background-shade: hsl(var(--background-hsl-hue), var(--background-hsl-saturation), 6%, 0.5) - --foreground-gray: rgb(102, 102, 102) --foreground-white: rgb(232, 227, 227) + --foreground-gray: rgb(102, 102, 102) --foreground-black: rgb(16, 16, 16) --black: rgb(20, 20, 20) --black-transparent: rgba(20, 20, 20, 0.2) --white: rgb(232, 227, 227) --white-transparent: rgba(232, 227, 227, 0.2) + --red: rgb(182, 100, 103) --red-transparent: rgba(182, 100, 103, 0.1) --orange: rgb(217, 140, 95) @@ -33,12 +34,17 @@ --blue-transparent: rgba(141, 163, 185, 0.1) --purple: rgb(169, 136, 176) --purple-transparent: rgba(169, 136, 176, 0.1) - --primary: rgb(183, 169, 151) + --primary-transparent: rgba(183, 169, 151, 0.1) + --warning: var(--orange) + --warning-transparent: var(--orange-transparent) --danger: var(--red) + --danger-transparent: var(--red-transparent) --success: var(--green) + --success-transparent: var(--green-transparent) --info: var(--blue) + --info-transparent: var(--blue-transparent) --rad: 0.4rem @@ -47,4 +53,4 @@ --breakpoint: 800px - --font-family: 'Rubik', sans-serif + --font-family: 'Switzer', sans-serif diff --git a/onlylegs/templates/base.html b/onlylegs/templates/base.html index 58b7bb2..403288f 100644 --- a/onlylegs/templates/base.html +++ b/onlylegs/templates/base.html @@ -17,9 +17,7 @@ <meta name="twitter:description" content="{{ config.WEBSITE_CONF.motto }}"> <!-- Fonts --> - <link rel="preconnect" href="https://fonts.googleapis.com"> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"> + <link href="https://api.fontshare.com/v2/css?f[]=switzer@101,600,701,800,501,601,900,100,700,901,400,201,401,200,300,301,801,500&display=swap" rel="stylesheet"> <!-- phosphor icons --> <script src="https://unpkg.com/@phosphor-icons/web"></script> @@ -130,6 +128,10 @@ <script type="text/javascript"> keepSquare(); + {% for message in get_flashed_messages() %} + addNotification('{{ message[0] }}', {{ message[1] }}); + {% endfor %} + const times = document.querySelectorAll('.time'); for (let i = 0; i < times.length; i++) { // Remove milliseconds @@ -191,10 +193,6 @@ } } } - - {% for message in get_flashed_messages() %} - addNotification('{{ message[0] }}', {{ message[1] }}); - {% endfor %} </script> {% block script %}{% endblock %} diff --git a/onlylegs/templates/profile.html b/onlylegs/templates/profile.html index 7880339..c0e42af 100644 --- a/onlylegs/templates/profile.html +++ b/onlylegs/templates/profile.html @@ -29,7 +29,7 @@ {% block header %} <div class="banner"> {% if user.banner %} - <img src="{{ url_for('static', filename='icon.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/> + <img src="{{ url_for('api.media', path='banner/' + user.banner) }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/> {% else %} <img src="{{ url_for('static', filename='banner.png') }}" alt="Profile Banner" onload="imgFade(this)" style="opacity:0;"/> {% endif %} diff --git a/onlylegs/templates/settings.html b/onlylegs/templates/settings.html index 88216b4..7cad7ab 100644 --- a/onlylegs/templates/settings.html +++ b/onlylegs/templates/settings.html @@ -22,15 +22,20 @@ <i class="ph ph-caret-down collapse-indicator"></i> </summary> - <form method="POST" action="{{ url_for('api.account_picture', user_id=current_user.id) }}" enctype="multipart/form-data"> + <form method="POST" action="{{ url_for('settings.account_picture') }}" enctype="multipart/form-data"> <h3>Profile Picture</h3> <input type="file" name="file" tab-index="-1"/> - <input type="submit" value="Upload" class="btn-block"> + <button type="submit" class="btn-block">Change Profile Picture</button> </form> - <form method="POST" action="{{ url_for('api.account_username', user_id=current_user.id) }}" enctype="multipart/form-data"> + <form method="POST" action="{{ url_for('settings.account_banner') }}" enctype="multipart/form-data"> + <h3>Profile Banner</h3> + <input type="file" name="file" tab-index="-1"/> + <button type="submit" class="btn-block">Change Profile Banner</button> + </form> + <form method="POST" action="{{ url_for('settings.account_username') }}" enctype="multipart/form-data"> <h3>Username</h3> <input type="text" name="name" class="input-block" value="{{ current_user.username }}" /> - <input type="submit" value="Upload" class="btn-block"/> + <button type="submit" class="btn-block">Change Username</button> </form> </details> @@ -40,10 +45,27 @@ <i class="ph ph-caret-down collapse-indicator"></i> </summary> - <form method="POST" action="" enctype="multipart/form-data"> - <h3>Email</h3> - <input type="text" name="email" class="input-block" value="{{ current_user.email }}" /> - <input type="submit" value="Upload" class="btn-block"/> - </form> + <form method="POST" action="{{ url_for('settings.account_email') }}" enctype="multipart/form-data"> + <h3>Email</h3> + <input type="text" name="email" class="input-block" value="{{ current_user.email }}" /> + <input type="password" name="current" class="input-block" placeholder="Current Password" /> + <button type="submit" class="btn-block">Change Email</button> + </form> + <form method="POST" action="{{ url_for('settings.account_password') }}" enctype="multipart/form-data"> + <h3>Password</h3> + <input type="password" name="current" class="input-block" placeholder="Current Password" /> + <input type="password" name="password" class="input-block" placeholder="New Password" /> + <input type="password" name="confirm" class="input-block" placeholder="Confirm Password" /> + <button type="submit" class="btn-block">Change Password</button> + </form> + </details> + + <details open> + <summary> + <i class="ph ph-info"></i><h2>Server</h2><span style="width: 100%"></span> + <i class="ph ph-caret-down collapse-indicator"></i> + </summary> + + <p>Nothing here :3</p> </details> {% endblock %} diff --git a/onlylegs/utils/startup.py b/onlylegs/utils/startup.py index 722b37f..c08fc3d 100644 --- a/onlylegs/utils/startup.py +++ b/onlylegs/utils/startup.py @@ -16,6 +16,7 @@ REQUIRED_DIRS = { "uploads": os.path.join(APPLICATION_ROOT, "media", "uploads"), "cache": os.path.join(APPLICATION_ROOT, "media", "cache"), "pfp": os.path.join(APPLICATION_ROOT, "media", "pfp"), + "banner": os.path.join(APPLICATION_ROOT, "media", "banner"), } EMAIL_REGEX = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") @@ -28,11 +29,10 @@ def check_dirs(): """ for directory in REQUIRED_DIRS.values(): - if os.path.exists(directory): - print("User directory already exists at:", directory) - return - os.makedirs(directory) - print("Created directory at:", directory) + if not os.path.exists(directory): + os.makedirs(directory) + print("Created directory at:", directory) + print("User directory already exists at:", directory) def check_env(): diff --git a/onlylegs/views/settings.py b/onlylegs/views/settings.py index 389433b..7bf360e 100644 --- a/onlylegs/views/settings.py +++ b/onlylegs/views/settings.py @@ -1,17 +1,159 @@ """ OnlyLegs - Settings page """ -from flask import Blueprint, render_template -from flask_login import login_required +import os +import pathlib +import re +import logging +from colorthief import ColorThief +from flask import ( + Blueprint, + request, + current_app, + render_template, + flash, + redirect, + url_for, +) +from flask_login import login_required, current_user +from werkzeug.security import check_password_hash, generate_password_hash +from onlylegs.extensions import db +from onlylegs.models import Users blueprint = Blueprint("settings", __name__, url_prefix="/settings") -@blueprint.route("/") +@blueprint.route("/", methods=["GET"]) @login_required def general(): """ General settings page """ return render_template("settings.html") + + +@blueprint.route("/account/pfp", methods=["POST"]) +@login_required +def account_picture(): + user_record = Users.query.filter_by(id=current_user.id).first() + uploaded_file = request.files.get("file", None) + if not uploaded_file: + return "No file uploaded!", 400 + + image_mime = pathlib.Path(uploaded_file.filename).suffix.replace(".", "").lower() + image_name = str(user_record.id) + "_pfp." + image_mime + image_path = os.path.join(current_app.config["PFP_FOLDER"], image_name) + + if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys(): + logging.info("File extension not allowed: %s", image_mime) + return "File extension not allowed", 403 + + if user_record.picture: + os.remove(os.path.join(current_app.config["PFP_FOLDER"], user_record.picture)) + cache_name = user_record.picture.rsplit(".")[0] + for file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob( + cache_name + "*" + ): + os.remove(file) + + uploaded_file.save(image_path) + image_colours = ColorThief(image_path).get_color() + + user_record.colour = image_colours + user_record.picture = image_name + db.session.commit() + + return "File uploaded", 200 + + +@blueprint.route("/account/banner", methods=["POST"]) +@login_required +def account_banner(): + user_record = Users.query.filter_by(id=current_user.id).first() + uploaded_file = request.files.get("file", None) + if not uploaded_file: + return "No file uploaded!", 400 + + image_mime = pathlib.Path(uploaded_file.filename).suffix.replace(".", "").lower() + image_name = str(user_record.id) + "_banner." + image_mime + image_path = os.path.join(current_app.config["BANNER_FOLDER"], image_name) + + if image_mime not in current_app.config["ALLOWED_EXTENSIONS"].keys(): + logging.info("File extension not allowed: %s", image_mime) + return "File extension not allowed", 403 + + if user_record.banner: + os.remove(os.path.join(current_app.config["BANNER_FOLDER"], user_record.banner)) + cache_name = user_record.banner.rsplit(".")[0] + for file in pathlib.Path(current_app.config["CACHE_FOLDER"]).glob( + cache_name + "*" + ): + os.remove(file) + + uploaded_file.save(image_path) + user_record.banner = image_name + db.session.commit() + + return "File uploaded", 200 + + +@blueprint.route("/account/username", methods=["POST"]) +@login_required +def account_username(): + user_record = Users.query.filter_by(id=current_user.id).first() + new_username = request.form.get("username", "").strip() + + username_regex = re.compile(r"\b[A-Za-z0-9._-]+\b") + + if not new_username or not username_regex.match(new_username): + return "Username is invalid", 400 + + user_record.username = new_username + db.session.commit() + + return "Username changed", 200 + + +@blueprint.route("/account/email", methods=["POST"]) +@login_required +def account_email(): + user_record = Users.query.filter_by(id=current_user.id).first() + current_password = request.form.get("current", "").strip() + new_email = request.form.get("email", "").strip() + + email_regex = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b") + + if not current_password or not new_email: + return "Fill in all the fields!", 400 + if not email_regex.match(new_email): + return "Email is invalid!", 400 + if not check_password_hash(user_record.password, current_password): + return "Incorrect password!", 400 + + user_record.email = new_email + db.session.commit() + + return "Email changed", 200 + + +@blueprint.route("/account/password", methods=["POST"]) +@login_required +def account_password(): + user_record = Users.query.filter_by(id=current_user.id).first() + current_password = request.form.get("current", "").strip() + new_password = request.form.get("password", "").strip() + new_confirm = request.form.get("confirm", "").strip() + + if not current_password or not new_password or not new_confirm: + return "Fill in all the fields!", 400 + if new_password != new_confirm: + return "Passwords do not match!", 400 + if not check_password_hash(user_record.password, current_password): + return "Incorrect password!", 400 + + user_record.password = generate_password_hash(new_password, method="scrypt") + db.session.commit() + + flash(["Password changed! You must login now", 0]) + return redirect(url_for("auth.logout"))