diff --git a/Dockerfile b/Dockerfile index fd45ad1a..43cf39c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8 as backend-dev +FROM python:3.10 as backend-dev ENV PYTHONUNBUFFERED=1 RUN useradd -m -d /opt/spacedock -s /bin/bash spacedock RUN pip3 install --upgrade pip setuptools wheel pip-licenses diff --git a/KerbalStuff/app.py b/KerbalStuff/app.py index 6de11f4a..4b91624f 100644 --- a/KerbalStuff/app.py +++ b/KerbalStuff/app.py @@ -14,11 +14,10 @@ import werkzeug.wrappers from flask import Flask, render_template, g, url_for, Response, request from flask_login import LoginManager, current_user -from flaskext.markdown import Markdown +from markupsafe import Markup from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.typing import ResponseReturnValue from jinja2 import ChainableUndefined -from pymdownx.emoji import gemoji, to_alt from .blueprints.accounts import accounts from .blueprints.admin import admin @@ -26,18 +25,16 @@ from .blueprints.api import api from .blueprints.blog import blog, get_all_announcement_posts, get_non_member_announcement_posts from .blueprints.lists import lists -from .blueprints.login_oauth import list_defined_oauths, login_oauth from .blueprints.mods import mods from .blueprints.profile import profiles from .middleware.session_interface import OnlyLoggedInSessionInterface from .celery import update_from_github -from .common import first_paragraphs, many_paragraphs, json_output, jsonify_exception, dumb_object, sanitize_text +from .common import first_paragraphs, many_paragraphs, json_output, jsonify_exception, dumb_object, sanitize_text, markdown_renderer from .config import _cfg, _cfgb, _cfgd, _cfgi, site_logger from .custom_json import CustomJSONEncoder from .database import db from .helpers import is_admin, following_mod -from .kerbdown import KerbDown -from .objects import User, BlogPost +from .objects import User app = Flask(__name__, template_folder='../templates') # https://flask.palletsprojects.com/en/1.1.x/security/#set-cookie-options @@ -59,15 +56,9 @@ app.jinja_env.filters['bleach'] = sanitize_text app.jinja_env.auto_reload = app.debug app.secret_key = _cfg("secret-key") -app.json_encoder = CustomJSONEncoder +app.json_encoder = CustomJSONEncoder # type: ignore[attr-defined] app.session_interface = OnlyLoggedInSessionInterface() -Markdown(app, extensions=[KerbDown(), 'fenced_code', 'pymdownx.emoji'], - extension_configs={'pymdownx.emoji': { - # GitHub's emojis - 'emoji_index': gemoji, - # Unicode output - 'emoji_generator': to_alt - }}) +app.jinja_env.filters['markdown'] = lambda md: Markup(markdown_renderer.convert(md)) login_manager = LoginManager(app) prof_dir = _cfg('profile-dir') @@ -95,7 +86,6 @@ def load_user(username: str) -> User: app.register_blueprint(profiles) app.register_blueprint(accounts) -app.register_blueprint(login_oauth) app.register_blueprint(anonymous) app.register_blueprint(blog) app.register_blueprint(admin) @@ -320,7 +310,6 @@ def inject() -> Dict[str, Any]: 'any': any, 'following_mod': following_mod, 'admin': is_admin(), - 'oauth_providers': list_defined_oauths(), 'dumb_object': dumb_object, 'first_visit': first_visit, 'request': request, diff --git a/KerbalStuff/blueprints/login_oauth.py b/KerbalStuff/blueprints/login_oauth.py deleted file mode 100644 index 6b0d4703..00000000 --- a/KerbalStuff/blueprints/login_oauth.py +++ /dev/null @@ -1,303 +0,0 @@ -import binascii -import os -from collections import OrderedDict -from typing import List, Dict, Optional, Union, Tuple, Any -import werkzeug.wrappers - -from flask import Blueprint, render_template, request, redirect, session, jsonify, url_for, \ - current_app -from flask_login import current_user, login_user -from flask_oauthlib.client import OAuth, OAuthRemoteApp - -from .accounts import check_username_for_registration, \ - check_email_for_registration -from ..config import _cfg -from ..database import db -from ..email import send_confirmation -from ..objects import User, UserAuth - -login_oauth = Blueprint('login_oauth', __name__) - - -# Python doesn't like OrderedDict with brackets, but mypy is fine with it -DEFINED_OAUTHS: Optional['OrderedDict[str, Dict[str, Any]]'] = None - - -def list_connected_oauths(user: User) -> List[str]: - return [a.provider for a in UserAuth.query.filter(UserAuth.user_id == user.id)] - - -def list_defined_oauths() -> 'OrderedDict[str, Dict[str, Any]]': - global DEFINED_OAUTHS - if DEFINED_OAUTHS is not None: - return DEFINED_OAUTHS - master_list = OrderedDict() - master_list['github'] = { - 'full_name': 'GitHub', - 'icon': 'github', - } - master_list['google'] = { - 'full_name': 'Google', - 'icon': 'google', - } - master_list['facebook'] = { - 'full_name': 'Facebook', - 'icon': 'facebook-official', - } - for p in list(master_list.keys()): - if not is_oauth_provider_configured(p): - del master_list[p] - DEFINED_OAUTHS = master_list - return DEFINED_OAUTHS - - -def is_oauth_provider_configured(provider: str) -> bool: - if provider == 'github': - return bool(_cfg('gh-oauth-id')) and bool(_cfg('gh-oauth-secret')) - if provider == 'google': - return (bool(_cfg('google-oauth-id')) and - bool(_cfg('google-oauth-secret'))) - return False - - -def get_github_oath() -> Tuple[str, OAuthRemoteApp]: - github = get_oauth_provider('github') - resp = github.authorized_response() - if resp is None: - raise Exception( - f"Access denied: reason={request.args['error']} error={request.args['error_description']}") - session['github_token'] = (resp['access_token'], '') - gh_info = github.get('user').data - return gh_info['login'], github - - -def _connect_with_oauth_finalize(remote_user: str, provider: str) -> Union[str, werkzeug.wrappers.Response]: - if not current_user: - return 'Trying to associate an account, but not logged in?' - auth = UserAuth.query.filter(UserAuth.provider == provider, - UserAuth.remote_user == remote_user).first() - if auth: - if auth.user_id == current_user.id: - # You're already set up. - return redirect('/profile/%s/edit' % current_user.username) - # This account is already connected with some user. - full_name = list_defined_oauths()[provider]['full_name'] - return 'Your %s account is already connected to a SpaceDock account.' % full_name - auth = UserAuth(user_id=current_user.id, - remote_user=remote_user, - provider=provider) - db.add(auth) - db.flush() # So that /profile will display currectly - return redirect('/profile/%s/edit' % current_user.username) - - -@login_oauth.route("/login-oauth", methods=['GET', 'POST']) -def login_with_oauth() -> Union[str, werkzeug.wrappers.Response]: - if request.method == 'GET': - return redirect('/login') - provider = request.form.get('provider', '') - if not is_oauth_provider_configured(provider): - return 'This install is not configured for login with %s' % provider - oauth = get_oauth_provider(provider) - callback = "{}://{}{}".format(_cfg("protocol"), _cfg("domain"), - url_for('.login_with_oauth_authorized_' + provider)) - return oauth.authorize(callback=callback) - - -@login_oauth.route("/connect-oauth", methods=['POST']) -def connect_with_oauth() -> Union[str, werkzeug.wrappers.Response]: - provider = request.form.get('provider', '') - if not is_oauth_provider_configured(provider): - return 'This install is not configured for login with %s' % provider - oauth = get_oauth_provider(provider) - callback = "{}://{}{}".format(_cfg("protocol"), _cfg("domain"), - url_for('.connect_with_oauth_authorized_' + provider)) - return oauth.authorize(callback=callback) - - -@login_oauth.route("/disconnect-oauth", methods=['POST']) -def disconnect_oauth() -> werkzeug.wrappers.Response: - provider = request.form.get('provider') - assert provider in list_defined_oauths() # This is a quick and dirty form of sanitation. - auths = UserAuth.query.filter(UserAuth.provider == provider, - UserAuth.user_id == current_user.id).all() - for auth in auths: - db.delete(auth) - db.flush() # So that /profile will display currectly - return redirect('/profile/%s/edit' % current_user.username) - - -@login_oauth.route("/oauth/github/connect") -def connect_with_oauth_authorized_github() -> Union[str, werkzeug.wrappers.Response]: - gh_user, _ = get_github_oath() - return _connect_with_oauth_finalize(gh_user, 'github') - - -@login_oauth.route("/oauth/google/connect") -def connect_with_oauth_authorized_google() -> Union[str, werkzeug.wrappers.Response]: - if 'code' not in request.args: - # Got here in some strange scenario. - return redirect('/') - google = get_oauth_provider('google') - resp = google.authorized_response() - if resp is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) - if 'error' in resp: - return jsonify(resp) - session['google_token'] = (resp['access_token'], '') - google_info = google.get('userinfo') - google_info = google_info.data - google_user = google_info['id'] # This is a long number. - return _connect_with_oauth_finalize(google_user, 'google') - - -@login_oauth.route("/oauth/github/login") -def login_with_oauth_authorized_github() -> Union[str, werkzeug.wrappers.Response]: - gh_user, github = get_github_oath() - auth = UserAuth.query.filter( - UserAuth.provider == 'github', - UserAuth.remote_user == gh_user).first() - if auth: - user = User.query.filter(User.id == auth.user_id).first() - if user.confirmation: - return redirect('/account-pending') - login_user(user, remember=True) - return redirect('/') - else: - emails = github.get('user/emails', []) - emails = emails.data - emails = [e['email'] for e in emails if e['primary']] - if emails: - email = emails[0] - else: - email = '' - return render_register_with_oauth('github', gh_user, gh_user, email) - - -@login_oauth.route("/oauth/google/login") -def login_with_oauth_authorized_google() -> Union[str, werkzeug.wrappers.Response]: - if 'code' not in request.args: - # Got here in some strange scenario. - return redirect('/') - google = get_oauth_provider('google') - resp = google.authorized_response() - - if resp is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) - if 'error' in resp: - return jsonify(resp.error) - session['google_token'] = (resp['access_token'], '') - google_info = google.get('userinfo') - google_info = google_info.data - google_user = google_info['id'] # This is a long number. - auth = UserAuth.query.filter( - UserAuth.provider == 'google', - UserAuth.remote_user == google_user).first() - if auth: - user = User.query.filter(User.id == auth.user_id).first() - if user.confirmation: - return redirect('/account-pending') - login_user(user, remember=True) - return redirect('/') - else: - email = google_info['email'] - username = email[:email.find('@')] - return render_register_with_oauth('google', google_user, username, email) - - -@login_oauth.route("/register-oauth", methods=['POST']) -def register_with_oauth_authorized() -> Union[str, werkzeug.wrappers.Response]: - """ - This endpoint should be called after authorizing with oauth, by the user. - """ - email = request.form.get('email', '') - username = request.form.get('username', '') - provider = request.form.get('provider', '') - remote_user = request.form.get('remote_user', '') - good = True - if check_username_for_registration(username): - good = False - if check_email_for_registration(email): - good = False - if good: - password = binascii.b2a_hex(os.urandom(99)) - user = User(username=username, email=email) - user.set_password(str(password)) - user.create_confirmation() - db.add(user) - db.flush() # to get an ID. - auth = UserAuth(user_id=user.id, - remote_user=remote_user, - provider=provider) - db.add(auth) - db.commit() # Commit before trying to email - send_confirmation(user) - return redirect("/account-pending") - return render_register_with_oauth(provider, remote_user, username, email) - - -def render_register_with_oauth(provider: str, remote_user: str, username: str, email: str) -> str: - provider_info = list_defined_oauths()[provider] - parameters = { - 'email': email, 'username': username, - 'provider': provider, - 'provider_full_name': provider_info['full_name'], - 'provider_icon': provider_info['icon'], - 'remote_user': remote_user - } - error = check_username_for_registration(username) - if error: - parameters['usernameError'] = error - error = check_email_for_registration(email) - if error: - parameters['emailError'] = error - return render_template('register-oauth.html', **parameters) - - -def get_oauth_provider(provider: str) -> OAuthRemoteApp: - oauth = OAuth(current_app) - if provider == 'github': - github = oauth.remote_app( - 'github', - consumer_key=_cfg('gh-oauth-id'), - consumer_secret=_cfg('gh-oauth-secret'), - request_token_params={'scope': 'user:email'}, - base_url='https://api.github.com/', - request_token_url=None, - access_token_method='POST', - access_token_url='https://github.com/login/oauth/access_token', - authorize_url='https://github.com/login/oauth/authorize' - ) - - @github.tokengetter - def get_github_oauth_token() -> str: - return session.get('github_token', '') - - return github - - if provider == 'google': - google = oauth.remote_app( - 'google', - consumer_key=_cfg('google-oauth-id'), - consumer_secret=_cfg('google-oauth-secret'), - request_token_params={'scope': 'email'}, - base_url='https://www.googleapis.com/oauth2/v1/', - request_token_url=None, - access_token_method='POST', - access_token_url='https://accounts.google.com/o/oauth2/token', - authorize_url='https://accounts.google.com/o/oauth2/auth', - ) - - @google.tokengetter - def get_google_oauth_token() -> str: - return session.get('google_token', '') - - return google - - raise Exception('This OAuth provider was not implemented: ' + provider) diff --git a/KerbalStuff/blueprints/profile.py b/KerbalStuff/blueprints/profile.py index 1cb66f60..89ccc8a1 100644 --- a/KerbalStuff/blueprints/profile.py +++ b/KerbalStuff/blueprints/profile.py @@ -6,7 +6,6 @@ from flask import Blueprint, render_template, abort, request, redirect from flask_login import current_user -from .login_oauth import list_connected_oauths, list_defined_oauths from ..common import loginrequired, with_session, sendfile, TRUE_STR from ..config import _cfg from ..objects import User, Following @@ -14,7 +13,7 @@ profiles = Blueprint('profile', __name__) FORUM_PROFILE_URL_PATTERN = re.compile( - r'^(?Phttps?://)?forum.kerbalspaceprogram.com/index.php\?/profile/(?P[0-9]+)-(?P[^/]+)') + r'^(?Phttps?://)?forum.kerbalspaceprogram.com(/index.php\?)?/profile/(?P[0-9]+)-(?P[^/]+)') @profiles.route("/profile/") @@ -88,17 +87,11 @@ def profile(username: str) -> Union[str, werkzeug.wrappers.Response]: if current_user != profile and not current_user.admin: abort(403) - extra_auths = list_connected_oauths(profile) - oauth_providers = list_defined_oauths() - for provider in oauth_providers: - oauth_providers[provider]['has_auth'] = provider in extra_auths - following = sorted(Following.query.filter(Following.user_id == profile.id), key=lambda fol: fol.mod.name.lower()) parameters = { 'profile': profile, - 'oauth_providers': oauth_providers, 'hide_login': current_user != profile, 'following': following, 'background': profile.background_url(_cfg('protocol'), _cfg('cdn-domain')), diff --git a/KerbalStuff/common.py b/KerbalStuff/common.py index 1e40bd28..c1cac67b 100644 --- a/KerbalStuff/common.py +++ b/KerbalStuff/common.py @@ -17,7 +17,8 @@ from markupsafe import Markup from werkzeug.exceptions import HTTPException from sqlalchemy.orm import Query -from markdown import markdown +from markdown import Markdown +from pymdownx.emoji import gemoji, to_alt from .config import _cfg from .custom_json import CustomJSONEncoder @@ -61,9 +62,19 @@ def sanitize_text(text: str) -> Markup: return Markup(cleaner.clean(text)) +markdown_renderer = Markdown( + extensions=[KerbDown(), 'fenced_code', 'pymdownx.emoji'], + extension_configs={'pymdownx.emoji': { + # GitHub's emojis + 'emoji_index': gemoji, + # Unicode output + 'emoji_generator': to_alt, +}}) + + def render_markdown(md: Optional[str]) -> Optional[Markup]: # The Markdown class is not thread-safe, sadly - return None if not md else sanitize_text(markdown(md, extensions=[KerbDown(), 'fenced_code'])) + return None if not md else sanitize_text(markdown_renderer.convert(md)) def dumb_object(model): # type: ignore diff --git a/KerbalStuff/custom_json.py b/KerbalStuff/custom_json.py index 04bee479..f83ed1cd 100644 --- a/KerbalStuff/custom_json.py +++ b/KerbalStuff/custom_json.py @@ -1,15 +1,15 @@ from datetime import datetime, timezone from typing import Any -from flask.json import JSONEncoder +from json import JSONEncoder class CustomJSONEncoder(JSONEncoder): - def default(self, obj: Any) -> Any: - if isinstance(obj, datetime): - return obj.astimezone(timezone.utc).isoformat() + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.astimezone(timezone.utc).isoformat() try: - return list(obj) + return list(o) except TypeError: pass - return super().default(obj) + return super().default(o) diff --git a/KerbalStuff/purge.py b/KerbalStuff/purge.py index d71a608e..7878f668 100644 --- a/KerbalStuff/purge.py +++ b/KerbalStuff/purge.py @@ -29,7 +29,9 @@ def purge_download(download_path: str) -> None: connection.create_connection = _orig_create_connection -def create_connection_cdn_purge(address: Tuple[str, Union[str, int, None]], *args: str, **kwargs: int) -> socket: +def create_connection_cdn_purge(address: Tuple[str, int], + *args: str, + **kwargs: int) -> socket: # Taken from https://stackoverflow.com/a/22614367 host, port = address @@ -41,4 +43,4 @@ def create_connection_cdn_purge(address: Tuple[str, Union[str, int, None]], *arg global _orig_create_connection assert callable(_orig_create_connection) - return _orig_create_connection((host, port), *args, **kwargs) + return _orig_create_connection((host, port), *args, **kwargs) # type: ignore[arg-type] diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md index 1ab16f80..9e9709c6 100644 --- a/README_DOCKERFILE.md +++ b/README_DOCKERFILE.md @@ -1,29 +1,37 @@ # How to use the Dockerfile + It should be assumed that all commands should be run from the project root, unless otherwise noted. Additionally, depending on your platform and how you've setup Docker, `docker` commands might need to be run as the `root` user or prefaced with `sudo` to work properly. ## Quickstart + ```sh ./start-server.sh ``` + Admin Credentials: admin:development User Credentials: user:development ## Install Docker + See for instructions on installing Docker for your platform. ## Configure, Build, and Run + The launch script will automatically copy the `config.ini.example`, `alembic.ini.example` and `logging.ini.example` to `config.ini`, `alembic.ini` and `logging.ini`, respectively. If you wish to provide your own, copy them like below and edit as you will. + ```sh cp config.ini.example config.ini && cp alembic.ini.example alembic.ini && cp logging.ini.example logging.ini ``` To create all the required containers and startup SpaceDock, run + ```sh ./start-server.sh ``` + This will automatically link `./` to `/opt/spacedock` in the backend container, as well as `./spacedock-db` into the database container and `./storage` into the frontend container. This means that changes you make locally to files in your project folder will be instantly synced to and reflected in the container (however CSS and JS files need to be rebuilt before the changes take effect). This will also forward port 5080 of the nginx container to port 5080 of your host, so you'll be able to browse to your local server. @@ -42,6 +50,7 @@ To interact with a container using the `docker` command, you need to use the con To interact using `docker-compose`, you need to use the service name and be in the repository directory or any subdirectory. ## Connecting + If you are on macOS or another environment that requires the use of docker-machine, then you must connect to the local server via that docker machine rather than localhost. To find out the correct IP to use in your browser, use `docker-machine ip`. You can then browse to port 5080 of that IP and all should be well. @@ -51,10 +60,12 @@ Admin Credentials: admin:development User Credentials: user:development ## Starting and Stopping + If you want to stop your container without losing any data, you can simply do `docker-compose stop`. Then, to start it back up, do `docker-compose up`. ## Odd and Ends + ```sh docker-compose exec backend /bin/bash # Start a bash shell in the backend container ``` diff --git a/config.ini.example b/config.ini.example index 11b795fb..e471b224 100644 --- a/config.ini.example +++ b/config.ini.example @@ -94,13 +94,6 @@ hook_repository=KSP-SpaceDock/SpaceDock hook_branch=master restart_command=systemd-restart.sh -# Various services you can plug in -# GitHub (Login) -gh-oauth-id= -gh-oauth-secret= -# Google (Login) -google-oauth-id= -google-oauth-secret= # Ads project_wonderful_id= # Analytics diff --git a/docker-compose.yml b/docker-compose.yml index 4870a6bc..e49e003d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,7 +52,6 @@ services: - 9999:9999 volumes: - ./:/opt/spacedock - - /dev/log:/dev/log links: - db - celery @@ -74,8 +73,6 @@ services: --loglevel=DEBUG --concurrency=1 -B - volumes: - - /dev/log:/dev/log links: - redis networks: diff --git a/oauth.md b/oauth.md deleted file mode 100644 index 6edff2b0..00000000 --- a/oauth.md +++ /dev/null @@ -1,49 +0,0 @@ -# How to configure OAuth - -All OAuth integrations require that you have a "domain" configured. - -In addition, you'll need to register an Application with each OAuth partner -(Provider), and update a shared secret in `config.ini`. - -## GitHub - -Go to Settings -> (select the relevant Organization if applicable) -> -Applications -> Developer Applications -> Register New Application. - -The first three fields are public information, and the last one - "Authorization - callback URL" - is where GitHub will send users after the login. Set it to: - - :///oauth/github/ - -For example, for KerbalStuff.com, it would be `https://kerbalstuff.com/oauth/github/`. - -Once you register, you'll get a "Client ID" and "Client Secret"; Set these values -in config.ini as `gh-oauth-id` and `gh-oauth-secret` respectively. - -## Google - -For google, the domain name has to end with ".com" or something like that, so -testing will be somewhat harder. -(Looks like it does like http://127.0.0.1:8080/, so maybe you're in luck) - -Visit https://console.developers.google.com/, and create a new project. - -Then: --> Use Google API --> (sidenav): Credentials --> OAuth consent screen --> Product name --> [SAVE] --> (sidenav): Credentials --> Credentials --> New Credentials -> OAuth Client ID --> web application --> name: kerbalstuff --> Authorized JavaScript origins: Leave blank --> Authorized redirect uris: Add these two: - - :///oauth/google/login - :///oauth/google/connect - -Set the Client ID (something-somethingsomething.apps.googleusercontent.com) as -`google-oauth-id` and Client Secret as `google-oauth-secret` in the config. diff --git a/requirements-backend.txt b/requirements-backend.txt index 7d5ce27a..6b607249 100644 --- a/requirements-backend.txt +++ b/requirements-backend.txt @@ -8,15 +8,12 @@ dnspython flameprof Flask Flask-Login -Flask-Markdown -Flask-OAuthlib future GitPython gunicorn Jinja2 -Markdown +markdown MarkupSafe -oauthlib packaging Pillow psycopg2-binary @@ -26,7 +23,6 @@ pymdown-extensions python-daemon redis requests -requests-oauthlib SQLAlchemy>=1.4,<2 # 1.4 shows deprecation warnings for 2.0, 2.0 would break our backend as of now SQLAlchemy-Utils tinycss2 diff --git a/templates/index.html b/templates/index.html index b0eacedb..4be1a1de 100644 --- a/templates/index.html +++ b/templates/index.html @@ -166,20 +166,6 @@

Register

  • Replace the "Log In" link with a "Log Out" link
  • And not much more!
  • - - {% for provider in oauth_providers %} - {% set provider_icon = oauth_providers[provider].icon %} - {% set provider_full_name = oauth_providers[provider].full_name %} -

    -

    - - -
    -

    - {% endfor %} - diff --git a/templates/layout.html b/templates/layout.html index 95eebcf4..505684ee 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -257,17 +257,6 @@ - {% for provider in oauth_providers %} - {% set provider_icon = oauth_providers[provider].icon %} - {% set provider_full_name = oauth_providers[provider].full_name %} -
    - - -
    - {% endfor %} Forgot Password diff --git a/templates/login.html b/templates/login.html index 719e9bb1..3ed10114 100644 --- a/templates/login.html +++ b/templates/login.html @@ -52,18 +52,6 @@

    Log In to {{ site_name }}

    Have you forgotten your password?

    Or maybe you want to create a new account?

    If you run into any trouble, please get in touch.

    - {% for provider in oauth_providers %} - {% set provider_icon = oauth_providers[provider].icon %} - {% set provider_full_name = oauth_providers[provider].full_name %} -
    -

    - - Or -

    -
    - {% endfor %} diff --git a/templates/profile.html b/templates/profile.html index d78be16d..66d23b2b 100644 --- a/templates/profile.html +++ b/templates/profile.html @@ -291,91 +291,7 @@

    Delete User Account

    -
    -
    -
    -

    Connected Accounts

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - {% if not oauth_providers %} - No services configured. - {% endif %} - {% for provider in oauth_providers %} - {% set provider_icon = oauth_providers[provider].icon %} - {% set provider_full_name = oauth_providers[provider].full_name %} - {% set auth = oauth_providers[provider].has_auth %} - -
    -
    -
    - - -
    -
    -

    - {% if auth %} - - {% else %} -
    - - -
    - {% endif %} -

    -
    -
    -
    - {% endfor %} -
    -
    -
    -

    - Connecting your account on another service allows you to log in with that service. -

    -
    -
    -
    -
    -
    {% endif %} -{% for provider in oauth_providers %} - {% set provider_icon = oauth_providers[provider].icon %} - {% set provider_full_name = oauth_providers[provider].full_name %} - -{% endfor %}