## First Commit ##
This commit is contained in:
commit
2fc7242290
|
|
@ -0,0 +1,7 @@
|
|||
FLASK_APP=project/__init__.py
|
||||
FLASK_DEBUG=1
|
||||
DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_dev
|
||||
SQL_HOST=db
|
||||
SQL_PORT=5432
|
||||
DATABASE=postgres
|
||||
APP_FOLDER=/usr/src/app
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
FLASK_APP=project/__init__.py
|
||||
FLASK_DEBUG=0
|
||||
DATABASE_URL=postgresql://hello_flask:hello_flask@db:5432/hello_flask_prod
|
||||
SQL_HOST=db
|
||||
SQL_PORT=5432
|
||||
DATABASE=postgres
|
||||
APP_FOLDER=/home/app/web
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
POSTGRES_USER=hello_flask
|
||||
POSTGRES_PASSWORD=hello_flask
|
||||
POSTGRES_DB=hello_flask_prod
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
*.pyc
|
||||
__pycache
|
||||
.DS_Store
|
||||
.env.dev
|
||||
.env.prod
|
||||
.env.prod.db
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 TestDriven.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Dockerizing Flask with Postgres, Gunicorn, and Nginx
|
||||
|
||||
## Want to learn how to build this?
|
||||
|
||||
Check out the [tutorial](https://testdriven.io/blog/dockerizing-flask-with-postgres-gunicorn-and-nginx).
|
||||
|
||||
## Want to use this project?
|
||||
|
||||
### Development
|
||||
|
||||
Uses the default Flask development server.
|
||||
|
||||
1. Rename *.env.dev-sample* to *.env.dev*.
|
||||
1. Update the environment variables in the *docker-compose.yml* and *.env.dev* files.
|
||||
- (M1 chip only) Remove `-slim-buster` from the Python dependency in `services/web/Dockerfile` to suppress an issue with installing psycopg2
|
||||
1. Build the images and run the containers:
|
||||
|
||||
```sh
|
||||
$ docker-compose up -d --build
|
||||
```
|
||||
|
||||
Test it out at [http://localhost:5000](http://localhost:5000). The "web" folder is mounted into the container and your code changes apply automatically.
|
||||
|
||||
### Production
|
||||
|
||||
Uses gunicorn + nginx.
|
||||
|
||||
1. Rename *.env.prod-sample* to *.env.prod* and *.env.prod.db-sample* to *.env.prod.db*. Update the environment variables.
|
||||
1. Build the images and run the containers:
|
||||
|
||||
```sh
|
||||
$ docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
Test it out at [http://localhost:1337](http://localhost:1337). No mounted folders. To apply changes, the image must be re-built.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: ./services/web
|
||||
dockerfile: Dockerfile.prod
|
||||
command: gunicorn --bind 0.0.0.0:5000 manage:app
|
||||
volumes:
|
||||
- static_volume:/home/app/web/project/static
|
||||
- media_volume:/home/app/web/project/media
|
||||
expose:
|
||||
- 5000
|
||||
env_file:
|
||||
- ./.env.prod-sample
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- postgres_data_prod:/var/lib/postgresql/data/
|
||||
env_file:
|
||||
- ./.env.prod.db-sample
|
||||
nginx:
|
||||
build: ./services/nginx
|
||||
volumes:
|
||||
- static_volume:/home/app/web/project/static
|
||||
- media_volume:/home/app/web/project/media
|
||||
ports:
|
||||
- 1337:80
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
volumes:
|
||||
postgres_data_prod:
|
||||
static_volume:
|
||||
media_volume:
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: ./services/web
|
||||
command: python manage.py run -h 0.0.0.0
|
||||
volumes:
|
||||
- ./services/web/:/usr/src/app/
|
||||
ports:
|
||||
- 5001:5000
|
||||
env_file:
|
||||
- ./.env.dev-sample
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:13
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
environment:
|
||||
- POSTGRES_USER=hello_flask
|
||||
- POSTGRES_PASSWORD=hello_flask
|
||||
- POSTGRES_DB=hello_flask_dev
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
FROM nginx:1.25
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
upstream hello_flask {
|
||||
server web:5000;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://hello_flask;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /home/app/web/project/static/;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /home/app/web/project/media/;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# pull official base image
|
||||
FROM python:3.11.3-slim-buster
|
||||
|
||||
# set work directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# install system dependencies
|
||||
RUN apt-get update && apt-get install -y netcat
|
||||
|
||||
# install dependencies
|
||||
RUN pip install --upgrade pip
|
||||
COPY ./requirements.txt /usr/src/app/requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# copy project
|
||||
COPY . /usr/src/app/
|
||||
|
||||
# run entrypoint.sh
|
||||
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
###########
|
||||
# BUILDER #
|
||||
###########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.11.3-slim-buster as builder
|
||||
|
||||
# set work directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# install system dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc
|
||||
|
||||
# lint
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install flake8==6.0.0
|
||||
COPY . /usr/src/app/
|
||||
# RUN flake8 --ignore=E501,F401 .
|
||||
|
||||
# install python dependencies
|
||||
COPY ./requirements.txt .
|
||||
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
|
||||
|
||||
|
||||
#########
|
||||
# FINAL #
|
||||
#########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.11.3-slim-buster
|
||||
|
||||
# create directory for the app user
|
||||
RUN mkdir -p /home/app
|
||||
|
||||
# create the app user
|
||||
RUN addgroup --system app && adduser --system --group app
|
||||
|
||||
# create the appropriate directories
|
||||
ENV HOME=/home/app
|
||||
ENV APP_HOME=/home/app/web
|
||||
RUN mkdir $APP_HOME
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
# install dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends netcat
|
||||
COPY --from=builder /usr/src/app/wheels /wheels
|
||||
COPY --from=builder /usr/src/app/requirements.txt .
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install --no-cache /wheels/*
|
||||
|
||||
# copy entrypoint-prod.sh
|
||||
COPY ./entrypoint.prod.sh $APP_HOME
|
||||
|
||||
# copy project
|
||||
COPY . $APP_HOME
|
||||
|
||||
# chown all the files to the app user
|
||||
RUN chown -R app:app $APP_HOME
|
||||
|
||||
# change to the app user
|
||||
USER app
|
||||
|
||||
# run entrypoint.prod.sh
|
||||
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import os
|
||||
|
||||
# from services.web.project import create_app, db
|
||||
# from sources.services.web.project import create_app, db
|
||||
from project import create_app,db
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=os.environ.get("FLASK_DEBUG"), host='0.0.0.0',port=os.environ.get("FLASK_SERVER_PORT"))
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ "$DATABASE" = "postgres" ]
|
||||
then
|
||||
echo "Waiting for postgres..."
|
||||
|
||||
while ! nc -z $SQL_HOST $SQL_PORT; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "PostgreSQL started"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ "$DATABASE" = "postgres" ]
|
||||
then
|
||||
echo "Waiting for postgres..."
|
||||
|
||||
while ! nc -z $SQL_HOST $SQL_PORT; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "PostgreSQL started"
|
||||
fi
|
||||
|
||||
python manage.py create_db
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from flask.cli import FlaskGroup
|
||||
|
||||
# from sources.services.web.app import app, db
|
||||
from .app import app, db
|
||||
# from project.models.users import User
|
||||
from project.models.users import User
|
||||
|
||||
|
||||
cli = FlaskGroup(app)
|
||||
|
||||
|
||||
@cli.command("create_db")
|
||||
def create_db():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
|
||||
@cli.command("seed_db")
|
||||
def seed_db():
|
||||
db.session.add(User(email="michael@mherman.org"))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
from flask import Flask
|
||||
import os
|
||||
|
||||
from .config import Config
|
||||
from .extensions import db
|
||||
from .libraries.flask_login_pum import LoginManager
|
||||
|
||||
|
||||
def create_app(config_class=Config):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
# login_manager.roles_view = 'main.profile'
|
||||
login_manager.init_app(app)
|
||||
|
||||
from .models.users import User
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id): #reload user object from the user ID stored in the session
|
||||
# since the user_id is just the primary key of our user table, use it in the query for the user
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
# Register blueprints here
|
||||
from .routes.main import bp as main_bp
|
||||
app.register_blueprint(main_bp)
|
||||
|
||||
from .routes.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp)
|
||||
|
||||
@app.route('/test/')
|
||||
def test_page():
|
||||
return '<h1>Board-Manager application is running, at least for this page !</h1>'
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import os
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
class Config(object):
|
||||
basedir_db = basedir + '/instance'
|
||||
# SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://")
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir_db, 'db.sqlite')
|
||||
print(SQLALCHEMY_DATABASE_URI)
|
||||
SECRET_KEY = 'board_manager_users'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
STATIC_FOLDER = f"{os.getenv('APP_FOLDER')}/project/static"
|
||||
MEDIA_FOLDER = f"{os.getenv('APP_FOLDER')}/project/media"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
__title__ = "Flask-Login"
|
||||
__description__ = "User session management for Flask"
|
||||
__url__ = "https://github.com/maxcountryman/flask-login"
|
||||
__version_info__ = ("0", "6", "3")
|
||||
__version__ = ".".join(__version_info__)
|
||||
__author__ = "Matthew Frazier"
|
||||
__author_email__ = "leafstormrush@gmail.com"
|
||||
__maintainer__ = "Max Countryman"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "(c) 2011 by Matthew Frazier"
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
from .__about__ import __version__
|
||||
from .config import AUTH_HEADER_NAME
|
||||
from .config import COOKIE_DURATION
|
||||
from .config import COOKIE_HTTPONLY
|
||||
from .config import COOKIE_NAME
|
||||
from .config import COOKIE_SECURE
|
||||
from .config import ID_ATTRIBUTE
|
||||
from .config import LOGIN_MESSAGE
|
||||
from .config import LOGIN_MESSAGE_CATEGORY
|
||||
from .config import REFRESH_MESSAGE
|
||||
from .config import REFRESH_MESSAGE_CATEGORY
|
||||
from .login_manager import LoginManager
|
||||
from .mixins import AnonymousUserMixin
|
||||
from .mixins import UserMixin
|
||||
from .signals import session_protected
|
||||
from .signals import user_accessed
|
||||
from .signals import user_loaded_from_cookie
|
||||
from .signals import user_loaded_from_request
|
||||
from .signals import user_logged_in
|
||||
from .signals import user_logged_out
|
||||
from .signals import user_login_confirmed
|
||||
from .signals import user_needs_refresh
|
||||
from .signals import user_unauthorized
|
||||
from .test_client import FlaskLoginClient
|
||||
from .utils import confirm_login
|
||||
from .utils import current_user
|
||||
from .utils import decode_cookie
|
||||
from .utils import encode_cookie
|
||||
from .utils import fresh_login_required
|
||||
from .utils import login_fresh
|
||||
from .utils import login_remembered
|
||||
from .utils import login_required
|
||||
from .utils import role_required
|
||||
from .utils import login_url
|
||||
from .utils import login_user
|
||||
from .utils import logout_user
|
||||
from .utils import make_next_param
|
||||
from .utils import set_login_view
|
||||
from .utils import set_roles_view
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"AUTH_HEADER_NAME",
|
||||
"COOKIE_DURATION",
|
||||
"COOKIE_HTTPONLY",
|
||||
"COOKIE_NAME",
|
||||
"COOKIE_SECURE",
|
||||
"ID_ATTRIBUTE",
|
||||
"LOGIN_MESSAGE",
|
||||
"LOGIN_MESSAGE_CATEGORY",
|
||||
"REFRESH_MESSAGE",
|
||||
"REFRESH_MESSAGE_CATEGORY",
|
||||
"LoginManager",
|
||||
"AnonymousUserMixin",
|
||||
"UserMixin",
|
||||
"session_protected",
|
||||
"user_accessed",
|
||||
"user_loaded_from_cookie",
|
||||
"user_loaded_from_request",
|
||||
"user_logged_in",
|
||||
"user_logged_out",
|
||||
"user_login_confirmed",
|
||||
"user_needs_refresh",
|
||||
"user_unauthorized",
|
||||
"FlaskLoginClient",
|
||||
"confirm_login",
|
||||
"current_user",
|
||||
"decode_cookie",
|
||||
"encode_cookie",
|
||||
"fresh_login_required",
|
||||
"login_fresh",
|
||||
"login_remembered",
|
||||
"login_required",
|
||||
"role_required",
|
||||
"login_url",
|
||||
"login_user",
|
||||
"logout_user",
|
||||
"make_next_param",
|
||||
"set_login_view",
|
||||
"set_roles_view",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "user_loaded_from_header":
|
||||
import warnings
|
||||
from .signals import _user_loaded_from_header
|
||||
|
||||
warnings.warn(
|
||||
"'user_loaded_from_header' is deprecated and will be"
|
||||
" removed in Flask-Login 0.7. Use"
|
||||
" 'user_loaded_from_request' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _user_loaded_from_header
|
||||
|
||||
raise AttributeError(name)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
from datetime import timedelta
|
||||
|
||||
#: The default name of the "remember me" cookie (``remember_token``)
|
||||
COOKIE_NAME = "remember_token"
|
||||
|
||||
#: The default time before the "remember me" cookie expires (365 days).
|
||||
COOKIE_DURATION = timedelta(days=365)
|
||||
|
||||
#: Whether the "remember me" cookie requires Secure; defaults to ``False``
|
||||
COOKIE_SECURE = False
|
||||
|
||||
#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``True``
|
||||
COOKIE_HTTPONLY = True
|
||||
|
||||
#: Whether the "remember me" cookie requires same origin; defaults to ``None``
|
||||
COOKIE_SAMESITE = None
|
||||
|
||||
#: The default flash message to display when users need to log in.
|
||||
LOGIN_MESSAGE = "Please log in to access this page."
|
||||
|
||||
ROLES_MESSAGE = "Your are not authorized to access this page."
|
||||
|
||||
#: The default flash message category to display when users need to log in.
|
||||
LOGIN_MESSAGE_CATEGORY = "message"
|
||||
|
||||
ROLES_MESSAGE_CATEGORY = "message"
|
||||
|
||||
#: The default flash message to display when users need to reauthenticate.
|
||||
REFRESH_MESSAGE = "Please reauthenticate to access this page."
|
||||
|
||||
#: The default flash message category to display when users need to
|
||||
#: reauthenticate.
|
||||
REFRESH_MESSAGE_CATEGORY = "message"
|
||||
|
||||
#: The default attribute to retreive the str id of the user
|
||||
ID_ATTRIBUTE = "get_id"
|
||||
|
||||
#: Default name of the auth header (``Authorization``)
|
||||
AUTH_HEADER_NAME = "Authorization"
|
||||
|
||||
#: A set of session keys that are populated by Flask-Login. Use this set to
|
||||
#: purge keys safely and accurately.
|
||||
SESSION_KEYS = {
|
||||
"_user_id",
|
||||
"_remember",
|
||||
"_remember_seconds",
|
||||
"_id",
|
||||
"_fresh",
|
||||
"next",
|
||||
}
|
||||
|
||||
#: A set of HTTP methods which are exempt from `login_required` and
|
||||
#: `fresh_login_required`. By default, this is just ``OPTIONS``.
|
||||
EXEMPT_METHODS = {"OPTIONS"}
|
||||
|
||||
#: If true, the page the user is attempting to access is stored in the session
|
||||
#: rather than a url parameter when redirecting to the login view; defaults to
|
||||
#: ``False``.
|
||||
USE_SESSION_FOR_NEXT = False
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import flash
|
||||
from flask import g
|
||||
from flask import has_app_context
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import session
|
||||
|
||||
from .config import AUTH_HEADER_NAME
|
||||
from .config import COOKIE_DURATION
|
||||
from .config import COOKIE_HTTPONLY
|
||||
from .config import COOKIE_NAME
|
||||
from .config import COOKIE_SAMESITE
|
||||
from .config import COOKIE_SECURE
|
||||
from .config import ID_ATTRIBUTE
|
||||
from .config import ROLES_MESSAGE
|
||||
from .config import LOGIN_MESSAGE
|
||||
from .config import ROLES_MESSAGE_CATEGORY
|
||||
from .config import LOGIN_MESSAGE_CATEGORY
|
||||
from .config import REFRESH_MESSAGE
|
||||
from .config import REFRESH_MESSAGE_CATEGORY
|
||||
from .config import SESSION_KEYS
|
||||
from .config import USE_SESSION_FOR_NEXT
|
||||
from .mixins import AnonymousUserMixin
|
||||
from .signals import session_protected
|
||||
from .signals import user_accessed
|
||||
from .signals import user_loaded_from_cookie
|
||||
from .signals import user_loaded_from_request
|
||||
from .signals import user_needs_refresh
|
||||
from .signals import user_unauthorized
|
||||
from .utils import _create_identifier
|
||||
from .utils import _user_context_processor
|
||||
from .utils import decode_cookie
|
||||
from .utils import encode_cookie
|
||||
from .utils import expand_login_view
|
||||
from .utils import login_url as make_login_url
|
||||
from .utils import make_next_param
|
||||
|
||||
|
||||
class LoginManager:
|
||||
"""This object is used to hold the settings used for logging in. Instances
|
||||
of :class:`LoginManager` are *not* bound to specific apps, so you can
|
||||
create one in the main body of your code and then bind it to your
|
||||
app in a factory function.
|
||||
"""
|
||||
|
||||
def __init__(self, app=None, add_context_processor=True):
|
||||
#: A class or factory function that produces an anonymous user, which
|
||||
#: is used when no one is logged in.
|
||||
self.anonymous_user = AnonymousUserMixin
|
||||
|
||||
#: The name of the view to redirect to when the user needs to log in.
|
||||
#: (This can be an absolute URL as well, if your authentication
|
||||
#: machinery is external to your application.)
|
||||
self.login_view = None
|
||||
|
||||
self.roles_view = None
|
||||
|
||||
#: Names of views to redirect to when the user needs to log in,
|
||||
#: per blueprint. If the key value is set to None the value of
|
||||
#: :attr:`login_view` will be used instead.
|
||||
self.blueprint_login_views = {}
|
||||
|
||||
self.blueprint_roles_views = {}
|
||||
|
||||
#: The message to flash when a user is redirected to the login page.
|
||||
self.login_message = LOGIN_MESSAGE
|
||||
self.roles_message = ROLES_MESSAGE
|
||||
|
||||
#: The message category to flash when a user is redirected to the login
|
||||
#: page.
|
||||
self.login_message_category = LOGIN_MESSAGE_CATEGORY
|
||||
self.roles_message_category = ROLES_MESSAGE_CATEGORY
|
||||
|
||||
#: The name of the view to redirect to when the user needs to
|
||||
#: reauthenticate.
|
||||
self.refresh_view = None
|
||||
|
||||
#: The message to flash when a user is redirected to the 'needs
|
||||
#: refresh' page.
|
||||
self.needs_refresh_message = REFRESH_MESSAGE
|
||||
|
||||
#: The message category to flash when a user is redirected to the
|
||||
#: 'needs refresh' page.
|
||||
self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
|
||||
|
||||
#: The mode to use session protection in. This can be either
|
||||
#: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
|
||||
#: it.
|
||||
self.session_protection = "basic"
|
||||
|
||||
#: If present, used to translate flash messages ``self.login_message``
|
||||
#: and ``self.needs_refresh_message``
|
||||
self.localize_callback = None
|
||||
|
||||
self.unauthorized_callback = None
|
||||
|
||||
self.needs_refresh_callback = None
|
||||
|
||||
self.id_attribute = ID_ATTRIBUTE
|
||||
|
||||
self._user_callback = None
|
||||
|
||||
self._header_callback = None
|
||||
|
||||
self._request_callback = None
|
||||
|
||||
self._session_identifier_generator = _create_identifier
|
||||
|
||||
if app is not None:
|
||||
self.init_app(app, add_context_processor)
|
||||
|
||||
def setup_app(self, app, add_context_processor=True): # pragma: no cover
|
||||
"""
|
||||
This method has been deprecated. Please use
|
||||
:meth:`LoginManager.init_app` instead.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'setup_app' is deprecated and will be removed in"
|
||||
" Flask-Login 0.7. Use 'init_app' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.init_app(app, add_context_processor)
|
||||
|
||||
def init_app(self, app, add_context_processor=True):
|
||||
"""
|
||||
Configures an application. This registers an `after_request` call, and
|
||||
attaches this `LoginManager` to it as `app.login_manager`.
|
||||
|
||||
:param app: The :class:`flask.Flask` object to configure.
|
||||
:type app: :class:`flask.Flask`
|
||||
:param add_context_processor: Whether to add a context processor to
|
||||
the app that adds a `current_user` variable to the template.
|
||||
Defaults to ``True``.
|
||||
:type add_context_processor: bool
|
||||
"""
|
||||
app.login_manager = self
|
||||
app.after_request(self._update_remember_cookie)
|
||||
|
||||
if add_context_processor:
|
||||
app.context_processor(_user_context_processor)
|
||||
|
||||
def unauthorized(self):
|
||||
"""
|
||||
This is called when the user is required to log in. If you register a
|
||||
callback with :meth:`LoginManager.unauthorized_handler`, then it will
|
||||
be called. Otherwise, it will take the following actions:
|
||||
|
||||
- Flash :attr:`LoginManager.login_message` to the user.
|
||||
|
||||
- If the app is using blueprints find the login view for
|
||||
the current blueprint using `blueprint_login_views`. If the app
|
||||
is not using blueprints or the login view for the current
|
||||
blueprint is not specified use the value of `login_view`.
|
||||
|
||||
- Redirect the user to the login view. (The page they were
|
||||
attempting to access will be passed in the ``next`` query
|
||||
string variable, so you can redirect there if present instead
|
||||
of the homepage. Alternatively, it will be added to the session
|
||||
as ``next`` if USE_SESSION_FOR_NEXT is set.)
|
||||
|
||||
If :attr:`LoginManager.login_view` is not defined, then it will simply
|
||||
raise a HTTP 401 (Unauthorized) error instead.
|
||||
|
||||
This should be returned from a view or before/after_request function,
|
||||
otherwise the redirect will have no effect.
|
||||
"""
|
||||
user_unauthorized.send(current_app._get_current_object())
|
||||
|
||||
if self.unauthorized_callback:
|
||||
return self.unauthorized_callback()
|
||||
|
||||
if request.blueprint in self.blueprint_login_views:
|
||||
login_view = self.blueprint_login_views[request.blueprint]
|
||||
else:
|
||||
login_view = self.login_view
|
||||
|
||||
if not login_view:
|
||||
abort(401)
|
||||
|
||||
if self.login_message:
|
||||
if self.localize_callback is not None:
|
||||
flash(
|
||||
self.localize_callback(self.login_message),
|
||||
category=self.login_message_category,
|
||||
)
|
||||
else:
|
||||
flash(self.login_message, category=self.login_message_category)
|
||||
|
||||
config = current_app.config
|
||||
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||
login_url = expand_login_view(login_view)
|
||||
session["_id"] = self._session_identifier_generator()
|
||||
session["next"] = make_next_param(login_url, request.url)
|
||||
redirect_url = make_login_url(login_view)
|
||||
else:
|
||||
redirect_url = make_login_url(login_view, next_url=request.url)
|
||||
|
||||
return redirect(redirect_url)
|
||||
|
||||
def roles_unauthorized(self):
|
||||
user_unauthorized.send(current_app._get_current_object())
|
||||
|
||||
if self.unauthorized_callback:
|
||||
return self.unauthorized_callback()
|
||||
|
||||
if request.blueprint in self.blueprint_roles_views:
|
||||
roles_view = self.blueprint_roles_views[request.blueprint]
|
||||
else:
|
||||
roles_view = self.roles_view
|
||||
|
||||
if not roles_view:
|
||||
abort(401)
|
||||
|
||||
if self.login_message:
|
||||
if self.localize_callback is not None:
|
||||
flash(
|
||||
self.localize_callback(self.roles_message),
|
||||
category=self.roles_message_category,
|
||||
)
|
||||
else:
|
||||
flash(self.roles_message, category=self.roles_message_category)
|
||||
|
||||
redirect_url = make_login_url(roles_view, next_url=request.url)
|
||||
|
||||
return redirect(redirect_url)
|
||||
|
||||
def user_loader(self, callback):
|
||||
"""
|
||||
This sets the callback for reloading a user from the session. The
|
||||
function you set should take a user ID (a ``str``) and return a
|
||||
user object, or ``None`` if the user does not exist.
|
||||
|
||||
:param callback: The callback for retrieving a user object.
|
||||
:type callback: callable
|
||||
"""
|
||||
self._user_callback = callback
|
||||
return self.user_callback
|
||||
|
||||
@property
|
||||
def user_callback(self):
|
||||
"""Gets the user_loader callback set by user_loader decorator."""
|
||||
return self._user_callback
|
||||
|
||||
def request_loader(self, callback):
|
||||
"""
|
||||
This sets the callback for loading a user from a Flask request.
|
||||
The function you set should take Flask request object and
|
||||
return a user object, or `None` if the user does not exist.
|
||||
|
||||
:param callback: The callback for retrieving a user object.
|
||||
:type callback: callable
|
||||
"""
|
||||
self._request_callback = callback
|
||||
return self.request_callback
|
||||
|
||||
@property
|
||||
def request_callback(self):
|
||||
"""Gets the request_loader callback set by request_loader decorator."""
|
||||
return self._request_callback
|
||||
|
||||
def unauthorized_handler(self, callback):
|
||||
"""
|
||||
This will set the callback for the `unauthorized` method, which among
|
||||
other things is used by `login_required`. It takes no arguments, and
|
||||
should return a response to be sent to the user instead of their
|
||||
normal view.
|
||||
|
||||
:param callback: The callback for unauthorized users.
|
||||
:type callback: callable
|
||||
"""
|
||||
self.unauthorized_callback = callback
|
||||
return callback
|
||||
|
||||
def needs_refresh_handler(self, callback):
|
||||
"""
|
||||
This will set the callback for the `needs_refresh` method, which among
|
||||
other things is used by `fresh_login_required`. It takes no arguments,
|
||||
and should return a response to be sent to the user instead of their
|
||||
normal view.
|
||||
|
||||
:param callback: The callback for unauthorized users.
|
||||
:type callback: callable
|
||||
"""
|
||||
self.needs_refresh_callback = callback
|
||||
return callback
|
||||
|
||||
def needs_refresh(self):
|
||||
"""
|
||||
This is called when the user is logged in, but they need to be
|
||||
reauthenticated because their session is stale. If you register a
|
||||
callback with `needs_refresh_handler`, then it will be called.
|
||||
Otherwise, it will take the following actions:
|
||||
|
||||
- Flash :attr:`LoginManager.needs_refresh_message` to the user.
|
||||
|
||||
- Redirect the user to :attr:`LoginManager.refresh_view`. (The page
|
||||
they were attempting to access will be passed in the ``next``
|
||||
query string variable, so you can redirect there if present
|
||||
instead of the homepage.)
|
||||
|
||||
If :attr:`LoginManager.refresh_view` is not defined, then it will
|
||||
simply raise a HTTP 401 (Unauthorized) error instead.
|
||||
|
||||
This should be returned from a view or before/after_request function,
|
||||
otherwise the redirect will have no effect.
|
||||
"""
|
||||
user_needs_refresh.send(current_app._get_current_object())
|
||||
|
||||
if self.needs_refresh_callback:
|
||||
return self.needs_refresh_callback()
|
||||
|
||||
if not self.refresh_view:
|
||||
abort(401)
|
||||
|
||||
if self.needs_refresh_message:
|
||||
if self.localize_callback is not None:
|
||||
flash(
|
||||
self.localize_callback(self.needs_refresh_message),
|
||||
category=self.needs_refresh_message_category,
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
self.needs_refresh_message,
|
||||
category=self.needs_refresh_message_category,
|
||||
)
|
||||
|
||||
config = current_app.config
|
||||
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
|
||||
login_url = expand_login_view(self.refresh_view)
|
||||
session["_id"] = self._session_identifier_generator()
|
||||
session["next"] = make_next_param(login_url, request.url)
|
||||
redirect_url = make_login_url(self.refresh_view)
|
||||
else:
|
||||
login_url = self.refresh_view
|
||||
redirect_url = make_login_url(login_url, next_url=request.url)
|
||||
|
||||
return redirect(redirect_url)
|
||||
|
||||
def header_loader(self, callback):
|
||||
"""
|
||||
This function has been deprecated. Please use
|
||||
:meth:`LoginManager.request_loader` instead.
|
||||
|
||||
This sets the callback for loading a user from a header value.
|
||||
The function you set should take an authentication token and
|
||||
return a user object, or `None` if the user does not exist.
|
||||
|
||||
:param callback: The callback for retrieving a user object.
|
||||
:type callback: callable
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'header_loader' is deprecated and will be removed in"
|
||||
" Flask-Login 0.7. Use 'request_loader' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self._header_callback = callback
|
||||
return callback
|
||||
|
||||
def _update_request_context_with_user(self, user=None):
|
||||
"""Store the given user as ctx.user."""
|
||||
|
||||
if user is None:
|
||||
user = self.anonymous_user()
|
||||
|
||||
g._login_user = user
|
||||
|
||||
def _load_user(self):
|
||||
"""Loads user from session or remember_me cookie as applicable"""
|
||||
|
||||
if self._user_callback is None and self._request_callback is None:
|
||||
raise Exception(
|
||||
"Missing user_loader or request_loader. Refer to "
|
||||
"http://flask-login.readthedocs.io/#how-it-works "
|
||||
"for more info."
|
||||
)
|
||||
|
||||
user_accessed.send(current_app._get_current_object())
|
||||
|
||||
# Check SESSION_PROTECTION
|
||||
if self._session_protection_failed():
|
||||
return self._update_request_context_with_user()
|
||||
|
||||
user = None
|
||||
|
||||
# Load user from Flask Session
|
||||
user_id = session.get("_user_id")
|
||||
if user_id is not None and self._user_callback is not None:
|
||||
user = self._user_callback(user_id)
|
||||
|
||||
# Load user from Remember Me Cookie or Request Loader
|
||||
if user is None:
|
||||
config = current_app.config
|
||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||
header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)
|
||||
has_cookie = (
|
||||
cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||
)
|
||||
if has_cookie:
|
||||
cookie = request.cookies[cookie_name]
|
||||
user = self._load_user_from_remember_cookie(cookie)
|
||||
elif self._request_callback:
|
||||
user = self._load_user_from_request(request)
|
||||
elif header_name in request.headers:
|
||||
header = request.headers[header_name]
|
||||
user = self._load_user_from_header(header)
|
||||
|
||||
return self._update_request_context_with_user(user)
|
||||
|
||||
def _session_protection_failed(self):
|
||||
sess = session._get_current_object()
|
||||
ident = self._session_identifier_generator()
|
||||
|
||||
app = current_app._get_current_object()
|
||||
mode = app.config.get("SESSION_PROTECTION", self.session_protection)
|
||||
|
||||
if not mode or mode not in ["basic", "strong"]:
|
||||
return False
|
||||
|
||||
# if the sess is empty, it's an anonymous user or just logged out
|
||||
# so we can skip this
|
||||
if sess and ident != sess.get("_id", None):
|
||||
if mode == "basic" or sess.permanent:
|
||||
if sess.get("_fresh") is not False:
|
||||
sess["_fresh"] = False
|
||||
session_protected.send(app)
|
||||
return False
|
||||
elif mode == "strong":
|
||||
for k in SESSION_KEYS:
|
||||
sess.pop(k, None)
|
||||
|
||||
sess["_remember"] = "clear"
|
||||
session_protected.send(app)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _load_user_from_remember_cookie(self, cookie):
|
||||
user_id = decode_cookie(cookie)
|
||||
if user_id is not None:
|
||||
session["_user_id"] = user_id
|
||||
session["_fresh"] = False
|
||||
user = None
|
||||
if self._user_callback:
|
||||
user = self._user_callback(user_id)
|
||||
if user is not None:
|
||||
app = current_app._get_current_object()
|
||||
user_loaded_from_cookie.send(app, user=user)
|
||||
return user
|
||||
return None
|
||||
|
||||
def _load_user_from_header(self, header):
|
||||
if self._header_callback:
|
||||
user = self._header_callback(header)
|
||||
if user is not None:
|
||||
app = current_app._get_current_object()
|
||||
|
||||
from .signals import _user_loaded_from_header
|
||||
|
||||
_user_loaded_from_header.send(app, user=user)
|
||||
return user
|
||||
return None
|
||||
|
||||
def _load_user_from_request(self, request):
|
||||
if self._request_callback:
|
||||
user = self._request_callback(request)
|
||||
if user is not None:
|
||||
app = current_app._get_current_object()
|
||||
user_loaded_from_request.send(app, user=user)
|
||||
return user
|
||||
return None
|
||||
|
||||
def _update_remember_cookie(self, response):
|
||||
# Don't modify the session unless there's something to do.
|
||||
if "_remember" not in session and current_app.config.get(
|
||||
"REMEMBER_COOKIE_REFRESH_EACH_REQUEST"
|
||||
):
|
||||
session["_remember"] = "set"
|
||||
|
||||
if "_remember" in session:
|
||||
operation = session.pop("_remember", None)
|
||||
|
||||
if operation == "set" and "_user_id" in session:
|
||||
self._set_cookie(response)
|
||||
elif operation == "clear":
|
||||
self._clear_cookie(response)
|
||||
|
||||
return response
|
||||
|
||||
def _set_cookie(self, response):
|
||||
# cookie settings
|
||||
config = current_app.config
|
||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||
|
||||
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
|
||||
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
|
||||
samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)
|
||||
|
||||
if "_remember_seconds" in session:
|
||||
duration = timedelta(seconds=session["_remember_seconds"])
|
||||
else:
|
||||
duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)
|
||||
|
||||
# prepare data
|
||||
data = encode_cookie(str(session["_user_id"]))
|
||||
|
||||
if isinstance(duration, int):
|
||||
duration = timedelta(seconds=duration)
|
||||
|
||||
try:
|
||||
expires = datetime.utcnow() + duration
|
||||
except TypeError as e:
|
||||
raise Exception(
|
||||
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
||||
f" instead got: {duration}"
|
||||
) from e
|
||||
|
||||
# actually set it
|
||||
response.set_cookie(
|
||||
cookie_name,
|
||||
value=data,
|
||||
expires=expires,
|
||||
domain=domain,
|
||||
path=path,
|
||||
secure=secure,
|
||||
httponly=httponly,
|
||||
samesite=samesite,
|
||||
)
|
||||
|
||||
def _clear_cookie(self, response):
|
||||
config = current_app.config
|
||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||
domain = config.get("REMEMBER_COOKIE_DOMAIN")
|
||||
path = config.get("REMEMBER_COOKIE_PATH", "/")
|
||||
response.delete_cookie(cookie_name, domain=domain, path=path)
|
||||
|
||||
@property
|
||||
def _login_disabled(self):
|
||||
"""Legacy property, use app.config['LOGIN_DISABLED'] instead."""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'_login_disabled' is deprecated and will be removed in"
|
||||
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||
" instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if has_app_context():
|
||||
return current_app.config.get("LOGIN_DISABLED", False)
|
||||
return False
|
||||
|
||||
@_login_disabled.setter
|
||||
def _login_disabled(self, newvalue):
|
||||
"""Legacy property setter, use app.config['LOGIN_DISABLED'] instead."""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'_login_disabled' is deprecated and will be removed in"
|
||||
" Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'"
|
||||
" instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
current_app.config["LOGIN_DISABLED"] = newvalue
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
class UserMixin:
|
||||
"""
|
||||
This provides default implementations for the methods that Flask-Login
|
||||
expects user objects to have.
|
||||
"""
|
||||
|
||||
# Python 3 implicitly set __hash__ to None if we override __eq__
|
||||
# We set it back to its default implementation
|
||||
__hash__ = object.__hash__
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return self.is_active
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
try:
|
||||
return str(self.id)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("No `id` attribute - override `get_id`") from None
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Checks the equality of two `UserMixin` objects using `get_id`.
|
||||
"""
|
||||
if isinstance(other, UserMixin):
|
||||
return self.get_id() == other.get_id()
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
Checks the inequality of two `UserMixin` objects using `get_id`.
|
||||
"""
|
||||
equal = self.__eq__(other)
|
||||
if equal is NotImplemented:
|
||||
return NotImplemented
|
||||
return not equal
|
||||
|
||||
|
||||
class AnonymousUserMixin:
|
||||
"""
|
||||
This is the default object for representing an anonymous user.
|
||||
"""
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return True
|
||||
|
||||
def get_id(self):
|
||||
return
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from flask.signals import Namespace
|
||||
|
||||
_signals = Namespace()
|
||||
|
||||
#: Sent when a user is logged in. In addition to the app (which is the
|
||||
#: sender), it is passed `user`, which is the user being logged in.
|
||||
user_logged_in = _signals.signal("logged-in")
|
||||
|
||||
#: Sent when a user is logged out. In addition to the app (which is the
|
||||
#: sender), it is passed `user`, which is the user being logged out.
|
||||
user_logged_out = _signals.signal("logged-out")
|
||||
|
||||
#: Sent when the user is loaded from the cookie. In addition to the app (which
|
||||
#: is the sender), it is passed `user`, which is the user being reloaded.
|
||||
user_loaded_from_cookie = _signals.signal("loaded-from-cookie")
|
||||
|
||||
#: Sent when the user is loaded from the header. In addition to the app (which
|
||||
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||
_user_loaded_from_header = _signals.signal("loaded-from-header")
|
||||
|
||||
#: Sent when the user is loaded from the request. In addition to the app (which
|
||||
#: is the #: sender), it is passed `user`, which is the user being reloaded.
|
||||
user_loaded_from_request = _signals.signal("loaded-from-request")
|
||||
|
||||
#: Sent when a user's login is confirmed, marking it as fresh. (It is not
|
||||
#: called for a normal login.)
|
||||
#: It receives no additional arguments besides the app.
|
||||
user_login_confirmed = _signals.signal("login-confirmed")
|
||||
|
||||
#: Sent when the `unauthorized` method is called on a `LoginManager`. It
|
||||
#: receives no additional arguments besides the app.
|
||||
user_unauthorized = _signals.signal("unauthorized")
|
||||
|
||||
#: Sent when the `needs_refresh` method is called on a `LoginManager`. It
|
||||
#: receives no additional arguments besides the app.
|
||||
user_needs_refresh = _signals.signal("needs-refresh")
|
||||
|
||||
#: Sent whenever the user is accessed/loaded
|
||||
#: receives no additional arguments besides the app.
|
||||
user_accessed = _signals.signal("accessed")
|
||||
|
||||
#: Sent whenever session protection takes effect, and a session is either
|
||||
#: marked non-fresh or deleted. It receives no additional arguments besides
|
||||
#: the app.
|
||||
session_protected = _signals.signal("session-protected")
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "user_loaded_from_header":
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'user_loaded_from_header' is deprecated and will be"
|
||||
" removed in Flask-Login 0.7. Use"
|
||||
" 'user_loaded_from_request' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _user_loaded_from_header
|
||||
|
||||
raise AttributeError(name)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from flask.testing import FlaskClient
|
||||
|
||||
|
||||
class FlaskLoginClient(FlaskClient):
|
||||
"""
|
||||
A Flask test client that knows how to log in users
|
||||
using the Flask-Login extension.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user", None)
|
||||
fresh = kwargs.pop("fresh_login", True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user:
|
||||
with self.session_transaction() as sess:
|
||||
sess["_user_id"] = user.get_id()
|
||||
sess["_fresh"] = fresh
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
import hmac
|
||||
from functools import wraps
|
||||
from hashlib import sha512
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlunsplit
|
||||
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import has_request_context
|
||||
from flask import request
|
||||
from flask import session
|
||||
from flask import url_for
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
from .config import COOKIE_NAME
|
||||
from .config import EXEMPT_METHODS
|
||||
from .signals import user_logged_in
|
||||
from .signals import user_logged_out
|
||||
from .signals import user_login_confirmed
|
||||
|
||||
#: A proxy for the current user. If no user is logged in, this will be an
|
||||
#: anonymous user
|
||||
current_user = LocalProxy(lambda: _get_user())
|
||||
|
||||
|
||||
def encode_cookie(payload, key=None):
|
||||
"""
|
||||
This will encode a ``str`` value into a cookie, and sign that cookie
|
||||
with the app's secret key.
|
||||
|
||||
:param payload: The value to encode, as `str`.
|
||||
:type payload: str
|
||||
|
||||
:param key: The key to use when creating the cookie digest. If not
|
||||
specified, the SECRET_KEY value from app config will be used.
|
||||
:type key: str
|
||||
"""
|
||||
return f"{payload}|{_cookie_digest(payload, key=key)}"
|
||||
|
||||
|
||||
def decode_cookie(cookie, key=None):
|
||||
"""
|
||||
This decodes a cookie given by `encode_cookie`. If verification of the
|
||||
cookie fails, ``None`` will be implicitly returned.
|
||||
|
||||
:param cookie: An encoded cookie.
|
||||
:type cookie: str
|
||||
|
||||
:param key: The key to use when creating the cookie digest. If not
|
||||
specified, the SECRET_KEY value from app config will be used.
|
||||
:type key: str
|
||||
"""
|
||||
try:
|
||||
payload, digest = cookie.rsplit("|", 1)
|
||||
if hasattr(digest, "decode"):
|
||||
digest = digest.decode("ascii") # pragma: no cover
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if hmac.compare_digest(_cookie_digest(payload, key=key), digest):
|
||||
return payload
|
||||
|
||||
|
||||
def make_next_param(login_url, current_url):
|
||||
"""
|
||||
Reduces the scheme and host from a given URL so it can be passed to
|
||||
the given `login` URL more efficiently.
|
||||
|
||||
:param login_url: The login URL being redirected to.
|
||||
:type login_url: str
|
||||
:param current_url: The URL to reduce.
|
||||
:type current_url: str
|
||||
"""
|
||||
l_url = urlsplit(login_url)
|
||||
c_url = urlsplit(current_url)
|
||||
|
||||
if (not l_url.scheme or l_url.scheme == c_url.scheme) and (
|
||||
not l_url.netloc or l_url.netloc == c_url.netloc
|
||||
):
|
||||
return urlunsplit(("", "", c_url.path, c_url.query, ""))
|
||||
return current_url
|
||||
|
||||
|
||||
def expand_login_view(login_view):
|
||||
"""
|
||||
Returns the url for the login view, expanding the view name to a url if
|
||||
needed.
|
||||
|
||||
:param login_view: The name of the login view or a URL for the login view.
|
||||
:type login_view: str
|
||||
"""
|
||||
if login_view.startswith(("https://", "http://", "/")):
|
||||
return login_view
|
||||
|
||||
return url_for(login_view)
|
||||
|
||||
|
||||
def login_url(login_view, next_url=None, next_field="next"):
|
||||
"""
|
||||
Creates a URL for redirecting to a login page. If only `login_view` is
|
||||
provided, this will just return the URL for it. If `next_url` is provided,
|
||||
however, this will append a ``next=URL`` parameter to the query string
|
||||
so that the login view can redirect back to that URL. Flask-Login's default
|
||||
unauthorized handler uses this function when redirecting to your login url.
|
||||
To force the host name used, set `FORCE_HOST_FOR_REDIRECTS` to a host. This
|
||||
prevents from redirecting to external sites if request headers Host or
|
||||
X-Forwarded-For are present.
|
||||
|
||||
:param login_view: The name of the login view. (Alternately, the actual
|
||||
URL to the login view.)
|
||||
:type login_view: str
|
||||
:param next_url: The URL to give the login view for redirection.
|
||||
:type next_url: str
|
||||
:param next_field: What field to store the next URL in. (It defaults to
|
||||
``next``.)
|
||||
:type next_field: str
|
||||
"""
|
||||
base = expand_login_view(login_view)
|
||||
|
||||
if next_url is None:
|
||||
return base
|
||||
|
||||
parsed_result = urlsplit(base)
|
||||
md = parse_qs(parsed_result.query, keep_blank_values=True)
|
||||
md[next_field] = make_next_param(base, next_url)
|
||||
netloc = current_app.config.get("FORCE_HOST_FOR_REDIRECTS") or parsed_result.netloc
|
||||
parsed_result = parsed_result._replace(
|
||||
netloc=netloc, query=urlencode(md, doseq=True)
|
||||
)
|
||||
return urlunsplit(parsed_result)
|
||||
|
||||
|
||||
def login_fresh():
|
||||
"""
|
||||
This returns ``True`` if the current login is fresh.
|
||||
"""
|
||||
return session.get("_fresh", False)
|
||||
|
||||
|
||||
def login_remembered():
|
||||
"""
|
||||
This returns ``True`` if the current login is remembered across sessions.
|
||||
"""
|
||||
config = current_app.config
|
||||
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||
has_cookie = cookie_name in request.cookies and session.get("_remember") != "clear"
|
||||
if has_cookie:
|
||||
cookie = request.cookies[cookie_name]
|
||||
user_id = decode_cookie(cookie)
|
||||
return user_id is not None
|
||||
return False
|
||||
|
||||
|
||||
def login_user(user, remember=False, duration=None, force=False, fresh=True):
|
||||
"""
|
||||
Logs a user in. You should pass the actual user object to this. If the
|
||||
user's `is_active` property is ``False``, they will not be logged in
|
||||
unless `force` is ``True``.
|
||||
|
||||
This will return ``True`` if the log in attempt succeeds, and ``False`` if
|
||||
it fails (i.e. because the user is inactive).
|
||||
|
||||
:param user: The user object to log in.
|
||||
:type user: object
|
||||
:param remember: Whether to remember the user after their session expires.
|
||||
Defaults to ``False``.
|
||||
:type remember: bool
|
||||
:param duration: The amount of time before the remember cookie expires. If
|
||||
``None`` the value set in the settings is used. Defaults to ``None``.
|
||||
:type duration: :class:`datetime.timedelta`
|
||||
:param force: If the user is inactive, setting this to ``True`` will log
|
||||
them in regardless. Defaults to ``False``.
|
||||
:type force: bool
|
||||
:param fresh: setting this to ``False`` will log in the user with a session
|
||||
marked as not "fresh". Defaults to ``True``.
|
||||
:type fresh: bool
|
||||
"""
|
||||
if not force and not user.is_active:
|
||||
return False
|
||||
|
||||
user_id = getattr(user, current_app.login_manager.id_attribute)()
|
||||
session["_user_id"] = user_id
|
||||
session["_fresh"] = fresh
|
||||
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||
|
||||
if remember:
|
||||
session["_remember"] = "set"
|
||||
if duration is not None:
|
||||
try:
|
||||
# equal to timedelta.total_seconds() but works with Python 2.6
|
||||
session["_remember_seconds"] = (
|
||||
duration.microseconds
|
||||
+ (duration.seconds + duration.days * 24 * 3600) * 10**6
|
||||
) / 10.0**6
|
||||
except AttributeError as e:
|
||||
raise Exception(
|
||||
f"duration must be a datetime.timedelta, instead got: {duration}"
|
||||
) from e
|
||||
|
||||
current_app.login_manager._update_request_context_with_user(user)
|
||||
user_logged_in.send(current_app._get_current_object(), user=_get_user())
|
||||
return True
|
||||
|
||||
|
||||
def logout_user():
|
||||
"""
|
||||
Logs a user out. (You do not need to pass the actual user.) This will
|
||||
also clean up the remember me cookie if it exists.
|
||||
"""
|
||||
|
||||
user = _get_user()
|
||||
|
||||
if "_user_id" in session:
|
||||
session.pop("_user_id")
|
||||
|
||||
if "_fresh" in session:
|
||||
session.pop("_fresh")
|
||||
|
||||
if "_id" in session:
|
||||
session.pop("_id")
|
||||
|
||||
cookie_name = current_app.config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
|
||||
if cookie_name in request.cookies:
|
||||
session["_remember"] = "clear"
|
||||
if "_remember_seconds" in session:
|
||||
session.pop("_remember_seconds")
|
||||
|
||||
user_logged_out.send(current_app._get_current_object(), user=user)
|
||||
|
||||
current_app.login_manager._update_request_context_with_user()
|
||||
return True
|
||||
|
||||
|
||||
def confirm_login():
|
||||
"""
|
||||
This sets the current session as fresh. Sessions become stale when they
|
||||
are reloaded from a cookie.
|
||||
"""
|
||||
session["_fresh"] = True
|
||||
session["_id"] = current_app.login_manager._session_identifier_generator()
|
||||
user_login_confirmed.send(current_app._get_current_object())
|
||||
|
||||
|
||||
def login_required(func):
|
||||
"""
|
||||
If you decorate a view with this, it will ensure that the current user is
|
||||
logged in and authenticated before calling the actual view. (If they are
|
||||
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
|
||||
example::
|
||||
|
||||
@app.route('/post')
|
||||
@login_required
|
||||
def post():
|
||||
pass
|
||||
|
||||
If there are only certain times you need to require that your user is
|
||||
logged in, you can do so with::
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
...which is essentially the code that this function adds to your views.
|
||||
|
||||
It can be convenient to globally turn off authentication when unit testing.
|
||||
To enable this, if the application configuration variable `LOGIN_DISABLED`
|
||||
is set to `True`, this decorator will be ignored.
|
||||
|
||||
.. Note ::
|
||||
|
||||
Per `W3 guidelines for CORS preflight requests
|
||||
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||
|
||||
:param func: The view function to decorate.
|
||||
:type func: function
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||
pass
|
||||
elif not current_user.is_authenticated:
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
# flask 1.x compatibility
|
||||
# current_app.ensure_sync is only available in Flask >= 2.0
|
||||
if callable(getattr(current_app, "ensure_sync", None)):
|
||||
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
def role_required(admin=False,role_names=''):
|
||||
# def role_required(*role_names):
|
||||
def wrapper(view_function):
|
||||
|
||||
@wraps(view_function)
|
||||
def decorator(*args, **kwargs):
|
||||
|
||||
if(admin == True and current_user.admin!=1):
|
||||
print('Authorization level not valid for the user {}'.format(current_user.name))
|
||||
return current_app.login_manager.roles_unauthorized()
|
||||
else:
|
||||
if(current_user.roles == role_names):
|
||||
print('Current user role {} is valid for {}'.format(current_user.roles, role_names))
|
||||
else:
|
||||
if(current_user.admin == 1):
|
||||
pass
|
||||
else:
|
||||
print('Current user role {} is not valid for {}'.format(current_user.roles, role_names))
|
||||
return current_app.login_manager.roles_unauthorized()
|
||||
|
||||
if callable(getattr(current_app, "ensure_sync", None)):
|
||||
return current_app.ensure_sync(view_function)(*args, **kwargs)
|
||||
|
||||
return view_function(*args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
return wrapper
|
||||
|
||||
def fresh_login_required(func):
|
||||
"""
|
||||
If you decorate a view with this, it will ensure that the current user's
|
||||
login is fresh - i.e. their session was not restored from a 'remember me'
|
||||
cookie. Sensitive operations, like changing a password or e-mail, should
|
||||
be protected with this, to impede the efforts of cookie thieves.
|
||||
|
||||
If the user is not authenticated, :meth:`LoginManager.unauthorized` is
|
||||
called as normal. If they are authenticated, but their session is not
|
||||
fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that
|
||||
case, you will need to provide a :attr:`LoginManager.refresh_view`.)
|
||||
|
||||
Behaves identically to the :func:`login_required` decorator with respect
|
||||
to configuration variables.
|
||||
|
||||
.. Note ::
|
||||
|
||||
Per `W3 guidelines for CORS preflight requests
|
||||
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
|
||||
HTTP ``OPTIONS`` requests are exempt from login checks.
|
||||
|
||||
:param func: The view function to decorate.
|
||||
:type func: function
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
|
||||
pass
|
||||
elif not current_user.is_authenticated:
|
||||
return current_app.login_manager.unauthorized()
|
||||
elif not login_fresh():
|
||||
return current_app.login_manager.needs_refresh()
|
||||
try:
|
||||
# current_app.ensure_sync available in Flask >= 2.0
|
||||
return current_app.ensure_sync(func)(*args, **kwargs)
|
||||
except AttributeError: # pragma: no cover
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_view
|
||||
|
||||
|
||||
def set_login_view(login_view, blueprint=None):
|
||||
"""
|
||||
Sets the login view for the app or blueprint. If a blueprint is passed,
|
||||
the login view is set for this blueprint on ``blueprint_login_views``.
|
||||
|
||||
:param login_view: The user object to log in.
|
||||
:type login_view: str
|
||||
:param blueprint: The blueprint which this login view should be set on.
|
||||
Defaults to ``None``.
|
||||
:type blueprint: object
|
||||
"""
|
||||
|
||||
num_login_views = len(current_app.login_manager.blueprint_login_views)
|
||||
if blueprint is not None or num_login_views != 0:
|
||||
(current_app.login_manager.blueprint_login_views[blueprint.name]) = login_view
|
||||
|
||||
if (
|
||||
current_app.login_manager.login_view is not None
|
||||
and None not in current_app.login_manager.blueprint_login_views
|
||||
):
|
||||
(
|
||||
current_app.login_manager.blueprint_login_views[None]
|
||||
) = current_app.login_manager.login_view
|
||||
|
||||
current_app.login_manager.login_view = None
|
||||
else:
|
||||
current_app.login_manager.login_view = login_view
|
||||
|
||||
def set_roles_view(roles_view, blueprint=None):
|
||||
"""
|
||||
Sets the login view for the app or blueprint. If a blueprint is passed,
|
||||
the login view is set for this blueprint on ``blueprint_login_views``.
|
||||
|
||||
:param login_view: The user object to log in.
|
||||
:type login_view: str
|
||||
:param blueprint: The blueprint which this login view should be set on.
|
||||
Defaults to ``None``.
|
||||
:type blueprint: object
|
||||
"""
|
||||
|
||||
num_roles_views = len(current_app.login_manager.blueprint_roles_views)
|
||||
if blueprint is not None or num_roles_views != 0:
|
||||
(current_app.login_manager.blueprint_roles_views[blueprint.name]) = roles_view
|
||||
|
||||
if (
|
||||
current_app.login_manager.roles_view is not None
|
||||
and None not in current_app.login_manager.blueprint_roles_views
|
||||
):
|
||||
(
|
||||
current_app.login_manager.blueprint_roles_views[None]
|
||||
) = current_app.login_manager.roles_view
|
||||
|
||||
current_app.login_manager.roles_view = None
|
||||
else:
|
||||
current_app.login_manager.roles_view = roles_view
|
||||
|
||||
def _get_user():
|
||||
if has_request_context():
|
||||
if "_login_user" not in g:
|
||||
current_app.login_manager._load_user()
|
||||
|
||||
return g._login_user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _cookie_digest(payload, key=None):
|
||||
key = _secret_key(key)
|
||||
|
||||
return hmac.new(key, payload.encode("utf-8"), sha512).hexdigest()
|
||||
|
||||
|
||||
def _get_remote_addr():
|
||||
address = request.headers.get("X-Forwarded-For", request.remote_addr)
|
||||
if address is not None:
|
||||
# An 'X-Forwarded-For' header includes a comma separated list of the
|
||||
# addresses, the first address being the actual remote address.
|
||||
address = address.encode("utf-8").split(b",")[0].strip()
|
||||
return address
|
||||
|
||||
|
||||
def _create_identifier():
|
||||
user_agent = request.headers.get("User-Agent")
|
||||
if user_agent is not None:
|
||||
user_agent = user_agent.encode("utf-8")
|
||||
base = f"{_get_remote_addr()}|{user_agent}"
|
||||
if str is bytes:
|
||||
base = str(base, "utf-8", errors="replace") # pragma: no cover
|
||||
h = sha512()
|
||||
h.update(base.encode("utf8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _user_context_processor():
|
||||
return dict(current_user=_get_user())
|
||||
|
||||
|
||||
def _secret_key(key=None):
|
||||
if key is None:
|
||||
key = current_app.config["SECRET_KEY"]
|
||||
|
||||
if isinstance(key, str): # pragma: no cover
|
||||
key = key.encode("latin1") # ensure bytes
|
||||
|
||||
return key
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# from services.web.project.libraries.flask_login_pum import UserMixin
|
||||
# from services.web.project import db
|
||||
|
||||
from ..libraries.flask_login_pum import UserMixin
|
||||
from .. import db
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
_table_args__ = {'extend_existing': True}
|
||||
id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
|
||||
email = db.Column(db.String(100), unique=True)
|
||||
password = db.Column(db.String(100))
|
||||
name = db.Column(db.String(1000))
|
||||
roles = db.Column(db.String(100), nullable=False, server_default='user')
|
||||
admin = db.Column(db.Integer, nullable=False, server_default='0')
|
||||
log_type = db.Column(db.String(10), nullable=False, server_default='none')
|
||||
plex_id = db.Column(db.Integer, nullable=False, server_default='0')
|
||||
blocked = db.Column(db.Integer, nullable=False, server_default='0')
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
from . import routes
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
########################################################################################
|
||||
###################### Import packages ###################################
|
||||
########################################################################################
|
||||
from flask import render_template, redirect, url_for, request, flash
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from ...models.users import User
|
||||
#from flask_login import login_user, logout_user, login_required, current_user
|
||||
import xml.dom.minidom
|
||||
|
||||
from . import bp
|
||||
from ...libraries.flask_login_pum import login_user, logout_user, login_required
|
||||
from ...extensions import db
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST']) # define login page path
|
||||
def login(): # define login page fucntion
|
||||
if request.method=='GET': # if the request is a GET we return the login page
|
||||
return render_template('auth/login.html')
|
||||
else: # if the request is POST the we check if the user exist and with te right password
|
||||
log_type = request.form.get('log_type')
|
||||
print(log_type)
|
||||
|
||||
email = request.form.get('email')
|
||||
# name = request.form.get('name')
|
||||
password = request.form.get('password')
|
||||
remember = True if request.form.get('remember') else False
|
||||
# user_name = User.query.filter_by(name=name).first()
|
||||
user_email = User.query.filter_by(name=email).first()
|
||||
|
||||
# print(user_name)
|
||||
print(user_email)
|
||||
# check if the user actually exists
|
||||
# take the user-supplied password, hash it, and compare it to the hashed password in the database
|
||||
if not user_email:
|
||||
flash('Not in user list!')
|
||||
# return redirect(url_for('auth.signup'))
|
||||
return redirect(url_for('auth.login'))
|
||||
elif not check_password_hash(user_email.password, password):
|
||||
flash('Please check your login details and try again.')
|
||||
return redirect(url_for('auth.login')) # if the user doesn't exist or password is wrong, reload the page
|
||||
# if the above check passes, then we know the user has the right credentials
|
||||
login_user(user_email, remember=remember)
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
|
||||
@bp.route('/signup', methods=['GET', 'POST'])# we define the sign up path
|
||||
def signup(): # define the sign up function
|
||||
if request.method=='GET': # If the request is GET we return the sign up page and forms
|
||||
return render_template('auth/signup.html')
|
||||
else: # if the request is POST, then we check if the email doesn't already exist and then we save data
|
||||
email = request.form.get('email')
|
||||
# name = request.form.get('name')
|
||||
password = request.form.get('password')
|
||||
user = User.query.filter_by(email=email).first() # if this returns a user, then the email already exists in database
|
||||
# user = User.query.filter_by(name=name).first() # if this returns a user, then the email already exists in database
|
||||
|
||||
if user: # if a user is found, we want to redirect back to signup page so user can try again
|
||||
flash('Name already exists')
|
||||
return redirect(url_for('auth.signup'))
|
||||
# create a new user with the form data. Hash the password so the plaintext version isn't saved.
|
||||
new_user = User(email=email, password=generate_password_hash(password, method='sha256'))
|
||||
# new_user = User(email=email, name=name, password=generate_password_hash(password, method='sha256'))
|
||||
# new_user = User(name=name, password=generate_password_hash(password, method='scrypt')) #
|
||||
# add the new user to the database
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@bp.route('/logout') # define logout path
|
||||
@login_required
|
||||
def logout(): #define the logout function
|
||||
logout_user()
|
||||
return redirect(url_for('main.index'))
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
from . import routes
|
||||
# from services.web.project.routes.main import routes
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from flask import render_template, redirect, url_for, request, flash
|
||||
from collections import OrderedDict
|
||||
|
||||
from . import bp
|
||||
from ...libraries.flask_login_pum import login_required, current_user
|
||||
|
||||
# from services.web.project.routes.main import bp
|
||||
# from services.web.project.libraries.flask_login_pum import login_required, current_user
|
||||
import datetime
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
print(current_user)
|
||||
print(current_user.is_authenticated)
|
||||
return render_template('main/index.html')
|
||||
|
||||
|
||||
@bp.route('/lrt')
|
||||
@login_required
|
||||
def login_required_test():
|
||||
print(current_user)
|
||||
print(current_user.is_authenticated)
|
||||
return '<h1>Board-Manager login required test !</h1>'
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
hi!
|
||||
rryt
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Functions to open and close a modal
|
||||
function openModal($el) {
|
||||
$el.classList.add('is-active');
|
||||
}
|
||||
|
||||
function closeModal($el) {
|
||||
$el.classList.remove('is-active');
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
|
||||
closeModal($modal);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
|
||||
const modal = $trigger.dataset.target;
|
||||
const $target = document.getElementById(modal);
|
||||
|
||||
$trigger.addEventListener('click', () => {
|
||||
openModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
|
||||
const $target = $close.closest('.modal');
|
||||
|
||||
$close.addEventListener('click', () => {
|
||||
closeModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a keyboard event to close all modals
|
||||
document.addEventListener('keydown', (event) => {
|
||||
const e = event || window.event;
|
||||
|
||||
if (e.keyCode === 27) { // Escape key
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
|
||||
const $notification = $delete.parentNode;
|
||||
|
||||
$delete.addEventListener('click', () => {
|
||||
$notification.parentNode.removeChild($notification);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Get all "navbar-burger" elements
|
||||
var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
// Check if there are any navbar burgers
|
||||
if ($navbarBurgers.length > 0) {
|
||||
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach(function ($el) {
|
||||
$el.addEventListener('click', function () {
|
||||
|
||||
// Get the target from the "data-target" attribute
|
||||
var target = $el.dataset.target;
|
||||
var $target = document.getElementById(target);
|
||||
|
||||
// console.log("el : " + $el)
|
||||
// console.log($el)
|
||||
// console.log("target : " + target)
|
||||
// console.log(target)
|
||||
// console.log("$target : " + $target)
|
||||
// console.log($target)
|
||||
|
||||
// Toggle the class on both the "navbar-burger" and the "navbar-menu"
|
||||
$el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// const bulmaCollapsibleInstances = bulmaCollapsible.attach('.is-collapsible');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const bulmaCollapsibleInstances = bulmaCollapsible.attach('.is-collapsible');
|
||||
let cardToggles2 = document.getElementsByClassName('card-header-icon');
|
||||
//console.log(cardToggles2)
|
||||
for (let i = 0; i < cardToggles2.length; i++) {
|
||||
cardToggles2[i].addEventListener('click', e => {
|
||||
|
||||
var element = e.currentTarget.parentElement.childNodes[3].childNodes[1].childNodes[1]
|
||||
|
||||
if(bulmaCollapsibleInstances[i].collapsed() == true){
|
||||
element.classList.replace('fa-angle-up','fa-angle-down')
|
||||
}
|
||||
else{
|
||||
element.classList.replace('fa-angle-down','fa-angle-up')
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,931 @@
|
|||
var p = {
|
||||
name: 'Unknown',
|
||||
version: 'Unknown',
|
||||
os: 'Unknown'
|
||||
};
|
||||
if (typeof platform !== 'undefined') {
|
||||
p.name = platform.name;
|
||||
p.version = platform.version;
|
||||
p.os = platform.os.toString();
|
||||
}
|
||||
|
||||
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
|
||||
if (!getCookie('browserDismiss')) {
|
||||
var $browser_warning = $('<div id="browser-warning">' +
|
||||
'<i class="fa fa-exclamation-circle"></i> ' +
|
||||
'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
|
||||
'Please use a different browser such as Chrome or Firefox.' +
|
||||
'<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
|
||||
'</div>');
|
||||
$('body').prepend($browser_warning);
|
||||
var offset = $browser_warning.height();
|
||||
warningOffset(offset);
|
||||
|
||||
$browser_warning.one('click', 'button.close', function () {
|
||||
$browser_warning.remove();
|
||||
warningOffset(-offset);
|
||||
setCookie('browserDismiss', 'true', 7);
|
||||
});
|
||||
|
||||
function warningOffset(offset) {
|
||||
var navbar = $('.navbar-fixed-top');
|
||||
if (navbar.length) {
|
||||
navbar.offset({top: navbar.offset().top + offset});
|
||||
}
|
||||
var container = $('.body-container');
|
||||
if (container.length) {
|
||||
container.offset({top: container.offset().top + offset});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initConfigCheckbox(elem, toggleElem, reverse) {
|
||||
toggleElem = (toggleElem === undefined) ? null : toggleElem;
|
||||
reverse = (reverse === undefined) ? false : reverse;
|
||||
var config = toggleElem ? $(toggleElem) : $(elem).closest('div').next();
|
||||
config.addClass('hidden-settings');
|
||||
if ($(elem).is(":checked")) {
|
||||
config.toggle(!reverse);
|
||||
} else {
|
||||
config.toggle(reverse);
|
||||
}
|
||||
$(elem).click(function () {
|
||||
var config = toggleElem ? $(toggleElem) : $(this).closest('div').next();
|
||||
if ($(this).is(":checked")) {
|
||||
config.slideToggleBool(!reverse);
|
||||
} else {
|
||||
config.slideToggleBool(reverse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshTab() {
|
||||
var url = $(location).attr('href');
|
||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||
$('.ui-tabs-panel:visible').load(url + " #" + tabId, function () {
|
||||
initThisPage();
|
||||
});
|
||||
}
|
||||
|
||||
function showMsg(msg, loader, timeout, ms, error) {
|
||||
var feedback = $("#ajaxMsg");
|
||||
var update = $("#updatebar");
|
||||
var token_error = $("#token_error_bar");
|
||||
if (update.is(":visible") || token_error.is(":visible")) {
|
||||
var height = (update.is(":visible") ? update.height() : 0) + (token_error.is(":visible") ? token_error.height() : 0) + 35;
|
||||
feedback.css("bottom", height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
var message = $("<div class='msg'>" + msg + "</div>");
|
||||
if (loader) {
|
||||
message = $("<div class='msg'><i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding", "14px 10px");
|
||||
}
|
||||
if (error) {
|
||||
feedback.css("background-color", "rgba(255,0,0,0.5)");
|
||||
}
|
||||
$(feedback).html(message);
|
||||
feedback.fadeIn();
|
||||
if (timeout) {
|
||||
setTimeout(function () {
|
||||
message.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut();
|
||||
feedback.css("background-color", "");
|
||||
});
|
||||
}, ms);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmAjaxCall(url, msg, data, loader_msg, callback) {
|
||||
$("#confirm-message").html(msg);
|
||||
$('#confirm-modal').modal();
|
||||
$('#confirm-modal').off('click', '#confirm-button').one('click', '#confirm-button', function () {
|
||||
if (loader_msg) {
|
||||
showMsg(loader_msg, true, false);
|
||||
}
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
async: true,
|
||||
data: data,
|
||||
complete: function (xhr, status) {
|
||||
var result = $.parseJSON(xhr.responseText);
|
||||
var msg = result.message;
|
||||
if (result.result == 'success') {
|
||||
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
|
||||
} else {
|
||||
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true);
|
||||
}
|
||||
if (typeof callback === "function") {
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
// Set Message
|
||||
var feedback = (showMsg) ? $("#ajaxMsg") : $();
|
||||
var update = $("#updatebar");
|
||||
var token_error = $("#token_error_bar");
|
||||
if (update.is(":visible") || token_error.is(":visible")) {
|
||||
var height = (update.is(":visible") ? update.height() : 0) + (token_error.is(":visible") ? token_error.height() : 0) + 35;
|
||||
feedback.css("bottom", height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
feedback.fadeIn();
|
||||
// Get Form data
|
||||
var formID = "#" + url;
|
||||
var dataString;
|
||||
if (form === true) {
|
||||
dataString = $(formID).serialize();
|
||||
}
|
||||
// Loader Image
|
||||
var loader = $("<div class='msg ajaxLoader-" + url +"'><i class='fa fa-refresh fa-spin'></i> Saving...</div>");
|
||||
// Data Success Message
|
||||
var dataSucces = $(elem).data('success');
|
||||
if (typeof dataSucces === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
dataSucces = "Success!";
|
||||
}
|
||||
// Data Error Message
|
||||
var dataError = $(elem).data('error');
|
||||
if (typeof dataError === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
dataError = "There was an error";
|
||||
}
|
||||
// Get Success & Error message from inline data, else use standard message
|
||||
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
|
||||
var errorMsg = $("<div class='msg'><i class='fa fa-exclamation-triangle'></i> " + dataError + "</div>");
|
||||
// Check if checkbox is selected
|
||||
if (form) {
|
||||
if ($('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ||
|
||||
$('#importLastFM #username:visible').length > 0 && $("#importLastFM #username").val().length === 0) {
|
||||
feedback.addClass('error');
|
||||
$(feedback).prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('error');
|
||||
});
|
||||
});
|
||||
$(formID + " select").children('option[disabled=disabled]').attr('selected', 'selected');
|
||||
}, 2000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Ajax Call
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: dataString,
|
||||
type: 'POST',
|
||||
beforeSend: function (jqXHR, settings) {
|
||||
// Start loader etc.
|
||||
feedback.prepend(loader);
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
feedback.addClass('error');
|
||||
feedback.prepend(errorMsg);
|
||||
setTimeout(function () {
|
||||
errorMsg.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('error');
|
||||
});
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
success: function (data, jqXHR) {
|
||||
feedback.prepend(succesMsg);
|
||||
feedback.addClass('success');
|
||||
setTimeout(function (e) {
|
||||
succesMsg.fadeOut(function () {
|
||||
$(this).remove();
|
||||
feedback.fadeOut(function () {
|
||||
feedback.removeClass('success');
|
||||
});
|
||||
if (reload === true) refreshSubmenu();
|
||||
if (reload === "table") {
|
||||
refreshTable();
|
||||
}
|
||||
if (reload === "tabs") refreshTab();
|
||||
if (reload === "page") location.reload();
|
||||
if (reload === "submenu&table") {
|
||||
refreshSubmenu();
|
||||
refreshTable();
|
||||
}
|
||||
if (form) {
|
||||
// Change the option to 'choose...'
|
||||
$(formID + " select").children('option[disabled=disabled]').attr(
|
||||
'selected', 'selected');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
complete: function (jqXHR, textStatus) {
|
||||
// Remove loaders and stuff, ajax request is complete!
|
||||
$('.ajaxLoader-' + url).remove();
|
||||
if (typeof callback === "function") {
|
||||
callback(jqXHR);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getBrowsePath = function (key, path, filter_ext) {
|
||||
var deferred = $.Deferred();
|
||||
|
||||
$.ajax({
|
||||
url: 'browse_path',
|
||||
type: 'GET',
|
||||
data: {
|
||||
key: key,
|
||||
path: path,
|
||||
filter_ext: filter_ext
|
||||
},
|
||||
success: function(data) {
|
||||
deferred.resolve(data);
|
||||
},
|
||||
error: function() {
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
return deferred;
|
||||
};
|
||||
|
||||
function doSimpleAjaxCall(url) {
|
||||
$.ajax(url);
|
||||
}
|
||||
|
||||
function resetFilters(text) {
|
||||
if ($(".dataTables_filter").length > 0) {
|
||||
$(".dataTables_filter input").attr("placeholder", "filter " + text + "");
|
||||
}
|
||||
}
|
||||
|
||||
function isPrivateIP(ip_address) {
|
||||
var defer = $.Deferred();
|
||||
|
||||
if (ipaddr.isValid(ip_address)) {
|
||||
var addr = ipaddr.process(ip_address);
|
||||
if (addr.range() === 'loopback' || addr.range() === 'private' || addr.range() === 'linkLocal') {
|
||||
defer.resolve();
|
||||
} else {
|
||||
defer.reject();
|
||||
}
|
||||
} else {
|
||||
defer.resolve('n/a');
|
||||
}
|
||||
|
||||
return defer.promise();
|
||||
}
|
||||
|
||||
function humanTime(seconds) {
|
||||
var d = Math.floor(moment.duration(seconds, 'seconds').asDays());
|
||||
var h = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours());
|
||||
var m = Math.round(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes());
|
||||
|
||||
var text = '';
|
||||
if (d > 0) {
|
||||
text = '<h3>' + d + '</h3><p> day' + ((d > 1) ? 's' : '') + '</p>'
|
||||
+ '<h3>' + h + '</h3><p> hr' + ((h > 1) ? 's' : '') + '</p>'
|
||||
+ '<h3>' + m + '</h3><p> min' + ((m > 1) ? 's' : '') + '</p>';
|
||||
} else if (h > 0) {
|
||||
text = '<h3>' + h + '</h3><p> hr' + ((h > 1) ? 's' : '') + '</p>'
|
||||
+ '<h3>' + m + '</h3><p> min' + ((m > 1) ? 's' : '') + '</p>';
|
||||
} else {
|
||||
text = '<h3>' + m + '</h3><p> min' + ((m > 1) ? 's' : '') + '</p>';
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
String.prototype.toProperCase = function () {
|
||||
return this.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
function getPercent(value1, value2) {
|
||||
value1 = parseFloat(value1) | 0
|
||||
value2 = parseFloat(value2) | 0
|
||||
|
||||
var percent = 0;
|
||||
if (value1 !== 0 && value2 !== 0) {
|
||||
percent = (value1 / value2) * 100
|
||||
}
|
||||
|
||||
return Math.round(percent)
|
||||
}
|
||||
|
||||
function millisecondsToMinutes(ms, roundToMinute) {
|
||||
if (ms > 0) {
|
||||
var minutes = Math.floor(ms / 60000);
|
||||
var seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||
if (roundToMinute) {
|
||||
return (seconds >= 30 ? (minutes + 1) : minutes);
|
||||
} else {
|
||||
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
|
||||
}
|
||||
} else {
|
||||
if (roundToMinute) {
|
||||
return '0';
|
||||
} else {
|
||||
return '0:00';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function humanDuration(ms, sig='dhm', units='ms', return_seconds=300000) {
|
||||
var factors = {
|
||||
d: 86400000,
|
||||
h: 3600000,
|
||||
m: 60000,
|
||||
s: 1000,
|
||||
ms: 1
|
||||
}
|
||||
|
||||
ms = parseInt(ms);
|
||||
var d, h, m, s;
|
||||
|
||||
if (ms > 0) {
|
||||
if (return_seconds && ms < return_seconds) {
|
||||
sig = 'dhms'
|
||||
}
|
||||
|
||||
ms = ms * factors[units];
|
||||
|
||||
h = ms % factors['d'];
|
||||
d = Math.trunc(ms / factors['d']);
|
||||
|
||||
m = h % factors['h'];
|
||||
h = Math.trunc(h / factors['h']);
|
||||
|
||||
s = m % factors['m'];
|
||||
m = Math.trunc(m / factors['m']);
|
||||
|
||||
ms = s % factors['s'];
|
||||
s = Math.trunc(s / factors['s']);
|
||||
|
||||
var hd_list = [];
|
||||
if (sig >= 'd' && d > 0) {
|
||||
d = (sig === 'd' && h >= 12) ? d + 1 : d;
|
||||
hd_list.push(d.toString() + ' day' + ((d > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dh' && h > 0) {
|
||||
h = (sig === 'dh' && m >= 30) ? h + 1 : h;
|
||||
hd_list.push(h.toString() + ' hr' + ((h > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dhm' && m > 0) {
|
||||
m = (sig === 'dhm' && s >= 30) ? m + 1 : m;
|
||||
hd_list.push(m.toString() + ' min' + ((m > 1) ? 's' : ''));
|
||||
}
|
||||
if (sig >= 'dhms' && s > 0) {
|
||||
hd_list.push(s.toString() + ' sec' + ((s > 1) ? 's' : ''));
|
||||
}
|
||||
|
||||
return hd_list.join(' ')
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
}
|
||||
|
||||
// Our countdown plugin takes a callback, a duration, and an optional message
|
||||
$.fn.countdown = function (callback, duration, message) {
|
||||
// If no message is provided, we use an empty string
|
||||
message = message || "";
|
||||
// Get reference to container, and set initial content
|
||||
var container = $(this[0]).html(duration + message);
|
||||
// Get reference to the interval doing the countdown
|
||||
var countdown = setInterval(function () {
|
||||
// If seconds remain
|
||||
if (--duration) {
|
||||
// Update our container's message
|
||||
container.html(duration + message);
|
||||
// Otherwise
|
||||
} else {
|
||||
// Clear the countdown interval
|
||||
clearInterval(countdown);
|
||||
// And fire the callback passing our container as `this`
|
||||
callback.call(container);
|
||||
}
|
||||
// Run interval every 1000ms (1 second)
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires;
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') c = c.substring(1);
|
||||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
var Accordion = function (el, multiple, close) {
|
||||
this.el = el || {};
|
||||
this.multiple = multiple || false;
|
||||
this.close = (close === undefined) ? true : close;
|
||||
// Variables privadas
|
||||
var links = this.el.find('.link');
|
||||
// Evento
|
||||
links.on('click', {
|
||||
el: this.el,
|
||||
multiple: this.multiple,
|
||||
close: this.close
|
||||
}, this.dropdown);
|
||||
};
|
||||
Accordion.prototype.dropdown = function (e) {
|
||||
var $el = e.data.el;
|
||||
$this = $(this);
|
||||
$next = $this.next();
|
||||
if (!e.data.close && $this.parent().hasClass('open')) {
|
||||
return
|
||||
}
|
||||
$next.slideToggle();
|
||||
$this.parent().toggleClass('open');
|
||||
if (!e.data.multiple) {
|
||||
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
||||
}
|
||||
};
|
||||
|
||||
function clearSearchButton(tableName, table) {
|
||||
$('#' + tableName + '_filter').find('input[type=search]').wrap(
|
||||
'<div class="input-group" role="group" aria-label="Search"></div>').after(
|
||||
'<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' +
|
||||
tableName + '"><i class="fa fa-remove"></i></button></span>');
|
||||
$('#clear-search-' + tableName).click(function () {
|
||||
table.search('').draw();
|
||||
});
|
||||
}
|
||||
// Taken from https://github.com/Hellowlol/HTPC-Manager
|
||||
window.onerror = function (message, file, line) {
|
||||
var e = {
|
||||
'page': window.location.href,
|
||||
'message': message,
|
||||
'file': file,
|
||||
'line': line
|
||||
};
|
||||
$.post("log_js_errors", e, function (data) { });
|
||||
};
|
||||
|
||||
$('*').on('click', '.refresh_pms_image', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var background_div = $(this).parent().siblings(['style*=pms_image_proxy']).first();
|
||||
var pms_proxy_url = background_div.css('background-image');
|
||||
pms_proxy_url = /^url\((['"]?)(.*)\1\)$/.exec(pms_proxy_url);
|
||||
pms_proxy_url = pms_proxy_url ? pms_proxy_url[2] : ""; // If matched, retrieve url, otherwise ""
|
||||
|
||||
if (pms_proxy_url.indexOf('pms_image_proxy') === -1) {
|
||||
console.log('PMS image proxy url not found.');
|
||||
} else {
|
||||
background_div.css('background-image', 'none')
|
||||
$.ajax({
|
||||
url: pms_proxy_url,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
success: function () {
|
||||
background_div.css('background-image', 'url(' + pms_proxy_url + ')');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Taken from http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable#answer-14919494
|
||||
function humanFileSize(bytes, si = true) {
|
||||
//var thresh = si ? 1000 : 1024;
|
||||
var thresh = 1024; // Always divide by 2^10 but display SI units
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
var units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
var u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
// Force max/min in number inputs
|
||||
function forceMinMax(elem) {
|
||||
var min = parseInt(elem.attr('min'));
|
||||
var max = parseInt(elem.attr('max'));
|
||||
var val = parseInt(elem.val());
|
||||
var default_val = parseInt(elem.data('default'));
|
||||
if (isNaN(val)) {
|
||||
elem.val(default_val);
|
||||
}
|
||||
else if (min !== undefined && val < min) {
|
||||
elem.val(min);
|
||||
}
|
||||
else if (max !== undefined && val > max) {
|
||||
elem.val(max);
|
||||
}
|
||||
else {
|
||||
elem.val(val);
|
||||
}
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
$.fn.slideToggleBool = function(bool, options) {
|
||||
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
|
||||
};
|
||||
|
||||
function openPlexXML(endpoint, plextv, params) {
|
||||
var data = $.extend({endpoint: endpoint, plextv: plextv || false}, params);
|
||||
var query = new URLSearchParams(data)
|
||||
window.open('open_plex_xml?' + query.toString(), '_blank');
|
||||
}
|
||||
|
||||
function PopupCenter(url, title, w, h) {
|
||||
// Fixes dual-screen position Most browsers Firefox
|
||||
var dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : window.screenX;
|
||||
var dualScreenTop = window.screenTop != undefined ? window.screenTop : window.screenY;
|
||||
|
||||
var width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
var height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
|
||||
|
||||
var left = ((width / 2) - (w / 2)) + dualScreenLeft;
|
||||
var top = ((height / 2) - (h / 2)) + dualScreenTop;
|
||||
var newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);
|
||||
|
||||
// Puts focus on the newWindow
|
||||
if (window.focus) {
|
||||
newWindow.focus();
|
||||
}
|
||||
|
||||
return newWindow;
|
||||
}
|
||||
|
||||
|
||||
function setLocalStorage(key, value, path) {
|
||||
var key_path = key;
|
||||
if (path !== false) {
|
||||
key_path = key_path + '_' + window.location.pathname;
|
||||
}
|
||||
localStorage.setItem(key_path, value);
|
||||
}
|
||||
function getLocalStorage(key, default_value, path) {
|
||||
var key_path = key;
|
||||
if (path !== false) {
|
||||
key_path = key_path + '_' + window.location.pathname;
|
||||
}
|
||||
var value = localStorage.getItem(key_path);
|
||||
if (value !== null) {
|
||||
return value
|
||||
} else if (default_value !== undefined) {
|
||||
setLocalStorage(key, default_value, path);
|
||||
return default_value
|
||||
}
|
||||
}
|
||||
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
|
||||
var cryptoObj = window.crypto || window.msCrypto; // for IE 11
|
||||
return (c ^ cryptoObj.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
});
|
||||
}
|
||||
|
||||
function getPlexHeaders(clientID) {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'X-Plex-Product': 'PUM',
|
||||
'X-Plex-Version': 'Plex OAuth',
|
||||
'X-Plex-Client-Identifier': clientID ? clientID : getLocalStorage('Tautulli_ClientID', uuidv4(), false),
|
||||
'X-Plex-Platform': p.name,
|
||||
'X-Plex-Platform-Version': p.version,
|
||||
'X-Plex-Model': 'Plex OAuth',
|
||||
'X-Plex-Device': p.os,
|
||||
'X-Plex-Device-Name': p.name + ' (PUM)',
|
||||
'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height,
|
||||
'X-Plex-Language': 'fr'
|
||||
};
|
||||
}
|
||||
|
||||
var plex_oauth_window = null;
|
||||
const plex_oauth_loader = '<style>' +
|
||||
'.login-loader-container {' +
|
||||
'font-family: "Open Sans", Arial, sans-serif;' +
|
||||
'position: absolute;' +
|
||||
'top: 0;' +
|
||||
'right: 0;' +
|
||||
'bottom: 0;' +
|
||||
'left: 0;' +
|
||||
'}' +
|
||||
'.login-loader-message {' +
|
||||
'color: #282A2D;' +
|
||||
'text-align: center;' +
|
||||
'position: absolute;' +
|
||||
'left: 50%;' +
|
||||
'top: 25%;' +
|
||||
'transform: translate(-50%, -50%);' +
|
||||
'}' +
|
||||
'.login-loader {' +
|
||||
'border: 5px solid #ccc;' +
|
||||
'-webkit-animation: spin 1s linear infinite;' +
|
||||
'animation: spin 1s linear infinite;' +
|
||||
'border-top: 5px solid #282A2D;' +
|
||||
'border-radius: 50%;' +
|
||||
'width: 50px;' +
|
||||
'height: 50px;' +
|
||||
'position: relative;' +
|
||||
'left: calc(50% - 25px);' +
|
||||
'}' +
|
||||
'@keyframes spin {' +
|
||||
'0% { transform: rotate(0deg); }' +
|
||||
'100% { transform: rotate(360deg); }' +
|
||||
'}' +
|
||||
'</style>' +
|
||||
'<div class="login-loader-container">' +
|
||||
'<div class="login-loader-message">' +
|
||||
'<div class="login-loader"></div>' +
|
||||
'<br>' +
|
||||
'Redirecting to the Plex login page...' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
function closePlexOAuthWindow() {
|
||||
if (plex_oauth_window) {
|
||||
plex_oauth_window.close();
|
||||
}
|
||||
}
|
||||
|
||||
getPlexOAuthPin = function (clientID) {
|
||||
var x_plex_headers = getPlexHeaders(clientID);
|
||||
var deferred = $.Deferred();
|
||||
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/v2/pins?strong=true',
|
||||
type: 'POST',
|
||||
headers: x_plex_headers,
|
||||
success: function(data) {
|
||||
deferred.resolve({pin: data.id, code: data.code});
|
||||
},
|
||||
error: function() {
|
||||
closePlexOAuthWindow();
|
||||
deferred.reject();
|
||||
}
|
||||
});
|
||||
return deferred;
|
||||
};
|
||||
|
||||
var polling = null;
|
||||
|
||||
function PlexOAuth(success, error, pre, clientID) {
|
||||
if (typeof pre === "function") {
|
||||
pre()
|
||||
}
|
||||
closePlexOAuthWindow();
|
||||
plex_oauth_window = PopupCenter('', 'Plex-OAuth', 600, 700);
|
||||
$(plex_oauth_window.document.body).html(plex_oauth_loader);
|
||||
|
||||
getPlexOAuthPin(clientID).then(function (data) {
|
||||
var x_plex_headers = getPlexHeaders(clientID);
|
||||
const pin = data.pin;
|
||||
const code = data.code;
|
||||
|
||||
var oauth_params = {
|
||||
'clientID': x_plex_headers['X-Plex-Client-Identifier'],
|
||||
'context[device][product]': x_plex_headers['X-Plex-Product'],
|
||||
'context[device][version]': x_plex_headers['X-Plex-Version'],
|
||||
'context[device][platform]': x_plex_headers['X-Plex-Platform'],
|
||||
'context[device][platformVersion]': x_plex_headers['X-Plex-Platform-Version'],
|
||||
'context[device][device]': x_plex_headers['X-Plex-Device'],
|
||||
'context[device][deviceName]': x_plex_headers['X-Plex-Device-Name'],
|
||||
'context[device][model]': x_plex_headers['X-Plex-Model'],
|
||||
'context[device][screenResolution]': x_plex_headers['X-Plex-Device-Screen-Resolution'],
|
||||
'context[device][layout]': 'desktop',
|
||||
'code': code
|
||||
}
|
||||
|
||||
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params);
|
||||
polling = pin;
|
||||
|
||||
(function poll() {
|
||||
$.ajax({
|
||||
url: 'https://plex.tv/api/v2/pins/' + pin,
|
||||
type: 'GET',
|
||||
headers: x_plex_headers,
|
||||
success: function (data) {
|
||||
if (data.authToken){
|
||||
closePlexOAuthWindow();
|
||||
if (typeof success === "function") {
|
||||
success(data.authToken)
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
if (textStatus !== "timeout") {
|
||||
closePlexOAuthWindow();
|
||||
if (typeof error === "function") {
|
||||
error()
|
||||
}
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
if (!plex_oauth_window.closed && polling === pin){
|
||||
setTimeout(function() {poll()}, 1000);
|
||||
}
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
})();
|
||||
}, function () {
|
||||
closePlexOAuthWindow();
|
||||
if (typeof error === "function") {
|
||||
error()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function encodeData(data) {
|
||||
return Object.keys(data).map(function(key) {
|
||||
return [key, data[key]].map(encodeURIComponent).join("=");
|
||||
}).join("&");
|
||||
}
|
||||
|
||||
function page(endpoint, ...args) {
|
||||
let endpoints = {
|
||||
'pms_image_proxy': pms_image_proxy,
|
||||
'info': info_page,
|
||||
'library': library_page,
|
||||
'user': user_page
|
||||
};
|
||||
|
||||
var params = {};
|
||||
|
||||
if (endpoint in endpoints) {
|
||||
params = endpoints[endpoint](...args);
|
||||
}
|
||||
|
||||
return endpoint + '?' + $.param(params).replace(/'/g, '%27');
|
||||
}
|
||||
|
||||
function pms_image_proxy(img, rating_key, width, height, opacity, background, blur, fallback, refresh, clip, img_format) {
|
||||
var params = {};
|
||||
|
||||
if (img != null) { params.img = img; }
|
||||
if (rating_key != null) { params.rating_key = rating_key; }
|
||||
if (width != null) { params.width = width; }
|
||||
if (height != null) { params.height = height; }
|
||||
if (opacity != null) { params.opacity = opacity; }
|
||||
if (background != null) { params.background = background; }
|
||||
if (blur != null) { params.blur = blur; }
|
||||
if (fallback != null) { params.fallback = fallback; }
|
||||
if (refresh != null) { params.refresh = true; }
|
||||
if (clip != null) { params.clip = true; }
|
||||
if (img_format != null) { params.img_format = img_format; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function info_page(rating_key, guid, history, live) {
|
||||
var params = {};
|
||||
|
||||
if (live && history) {
|
||||
params.guid = guid;
|
||||
} else {
|
||||
params.rating_key = rating_key;
|
||||
}
|
||||
|
||||
if (history) { params.source = 'history'; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function library_page(section_id) {
|
||||
var params = {};
|
||||
|
||||
if (section_id != null) { params.section_id = section_id; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function user_page(user_id, user) {
|
||||
var params = {};
|
||||
|
||||
if (user_id != null) { params.user_id = user_id; }
|
||||
if (user != null) { params.user = user; }
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
MEDIA_TYPE_HEADERS = {
|
||||
'movie': 'Movies',
|
||||
'show': 'TV Shows',
|
||||
'season': 'Seasons',
|
||||
'episode': 'Episodes',
|
||||
'artist': 'Artists',
|
||||
'album': 'Albums',
|
||||
'track': 'Tracks',
|
||||
'video': 'Videos',
|
||||
'audio': 'Tracks',
|
||||
'photo': 'Photos'
|
||||
}
|
||||
|
||||
function short_season(title) {
|
||||
if (title.startsWith('Season ') && /^\d+$/.test(title.substring(7))) {
|
||||
return 'S' + title.substring(7)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
function loadAllBlurHash() {
|
||||
$('[data-blurhash]').each(function() {
|
||||
const elem = $(this);
|
||||
const src = elem.data('blurhash');
|
||||
loadBlurHash(elem, src);
|
||||
});
|
||||
}
|
||||
|
||||
function loadBlurHash(elem, src) {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
const imgData = blurhash.getImageData(img);
|
||||
|
||||
blurhash
|
||||
.encodePromise(imgData, img.width, img.height, 4, 4)
|
||||
.then(hash => {
|
||||
return blurhash.decodePromise(
|
||||
hash,
|
||||
img.width,
|
||||
img.height
|
||||
);
|
||||
})
|
||||
.then(blurhashImgData => {
|
||||
const imgObject = blurhash.getImageDataAsImage(
|
||||
blurhashImgData,
|
||||
img.width,
|
||||
img.height,
|
||||
(event, imgObject) => {
|
||||
elem.css('background-image', 'url(' + imgObject.src + ')')
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _toggleRevealToken(elem, click) {
|
||||
var input = elem.parent().siblings('input');
|
||||
if ((input.prop('type') === 'password' && click) || !input.val()) {
|
||||
input.prop('type', 'text');
|
||||
elem.children('.fa').removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
} else {
|
||||
input.prop('type', 'password');
|
||||
elem.children('.fa').removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
}
|
||||
}
|
||||
function toggleRevealTokens() {
|
||||
$('.reveal-token').each(function () {
|
||||
_toggleRevealToken($(this));
|
||||
});
|
||||
}
|
||||
|
||||
$('body').on('click', '.reveal-token', function() {
|
||||
_toggleRevealToken($(this), true);
|
||||
});
|
||||
|
||||
// https://stackoverflow.com/a/57414592
|
||||
// prevent modal close when click starts in modal and ends on backdrop
|
||||
$(document).on('mousedown', '.modal', function(e){
|
||||
window.clickStartedInModal = $(e.target).is('.modal-dialog *');
|
||||
});
|
||||
$(document).on('mouseup', '.modal', function(e){
|
||||
if(!$(e.target).is('.modal-dialog *') && window.clickStartedInModal) {
|
||||
window.preventModalClose = true;
|
||||
}
|
||||
});
|
||||
$('.modal').on('hide.bs.modal', function (e) {
|
||||
if(window.preventModalClose){
|
||||
window.preventModalClose = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$.fn.hasScrollBar = function() {
|
||||
return this.get(0).scrollHeight > this.get(0).clientHeight;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.is-collapsible{height:0;overflow-y:hidden;transition:height .2s ease}.is-collapsible.is-active{transition:height .2s ease}.is-collapsible.message-body{padding:0!important}.is-collapsible.message-body .message-body-content{padding:1.25em 1.5em}
|
||||
1
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-collapsible.min.css
vendored
Normal file
1
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-collapsible.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.is-collapsible{height:0;overflow-y:hidden;transition:height .2s ease}.is-collapsible.is-active{transition:height .2s ease}.is-collapsible.message-body{padding:0!important}.is-collapsible.message-body .message-body-content{padding:1.25em 1.5em}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/*! @creativebulma/bulma-divider v1.1.0 | (c) 2020 Gaetan | MIT License | https://github.com/CreativeBulma/bulma-divider */
|
||||
@-webkit-keyframes spinAround {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinAround {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* line 17, src/sass/app.sass */
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
color: #7a7a7a;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .5px;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
/* line 28, src/sass/app.sass */
|
||||
.divider::after, .divider::before {
|
||||
content: '';
|
||||
display: block;
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: #dbdbdb;
|
||||
}
|
||||
|
||||
/* line 37, src/sass/app.sass */
|
||||
.divider:not(.is-right)::after {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* line 41, src/sass/app.sass */
|
||||
.divider:not(.is-left)::before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* line 45, src/sass/app.sass */
|
||||
.divider.is-left::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* line 49, src/sass/app.sass */
|
||||
.divider.is-right::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* line 52, src/sass/app.sass */
|
||||
.divider.is-vertical {
|
||||
flex-direction: column;
|
||||
margin: 0 25px;
|
||||
}
|
||||
|
||||
/* line 56, src/sass/app.sass */
|
||||
.divider.is-vertical::after, .divider.is-vertical::before {
|
||||
height: auto;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/* line 61, src/sass/app.sass */
|
||||
.divider.is-vertical::after {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* line 65, src/sass/app.sass */
|
||||
.divider.is-vertical::before {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-white::after, .divider.is-white::before {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-black::after, .divider.is-black::before {
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-light::after, .divider.is-light::before {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-dark::after, .divider.is-dark::before {
|
||||
background-color: #363636;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-primary::after, .divider.is-primary::before {
|
||||
background-color: #00d1b2;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-primary.is-light::after, .divider.is-primary.is-light::before {
|
||||
background-color: #ebfffc;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-link::after, .divider.is-link::before {
|
||||
background-color: #3273dc;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-link.is-light::after, .divider.is-link.is-light::before {
|
||||
background-color: #eef3fc;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-info::after, .divider.is-info::before {
|
||||
background-color: #3298dc;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-info.is-light::after, .divider.is-info.is-light::before {
|
||||
background-color: #eef6fc;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-success::after, .divider.is-success::before {
|
||||
background-color: #48c774;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-success.is-light::after, .divider.is-success.is-light::before {
|
||||
background-color: #effaf3;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-warning::after, .divider.is-warning::before {
|
||||
background-color: #ffdd57;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-warning.is-light::after, .divider.is-warning.is-light::before {
|
||||
background-color: #fffbeb;
|
||||
}
|
||||
|
||||
/* line 72, src/sass/app.sass */
|
||||
.divider.is-danger::after, .divider.is-danger::before {
|
||||
background-color: #f14668;
|
||||
}
|
||||
|
||||
/* line 80, src/sass/app.sass */
|
||||
.divider.is-danger.is-light::after, .divider.is-danger.is-light::before {
|
||||
background-color: #feecf0;
|
||||
}
|
||||
2
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-divider.min.css
vendored
Normal file
2
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-divider.min.css
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/*! @creativebulma/bulma-divider v1.1.0 | (c) 2020 Gaetan | MIT License | https://github.com/CreativeBulma/bulma-divider */
|
||||
.divider{position:relative;display:flex;align-items:center;text-transform:uppercase;color:#7a7a7a;font-size:.75rem;font-weight:600;letter-spacing:.5px;margin:25px 0}.divider:after,.divider:before{content:"";display:block;flex:1;height:1px;background-color:#dbdbdb}.divider:not(.is-right):after{margin-left:10px}.divider:not(.is-left):before{margin-right:10px}.divider.is-left:before,.divider.is-right:after{display:none}.divider.is-vertical{flex-direction:column;margin:0 25px}.divider.is-vertical:after,.divider.is-vertical:before{height:auto;width:1px}.divider.is-vertical:after{margin-left:0;margin-top:10px}.divider.is-vertical:before{margin-right:0;margin-bottom:10px}.divider.is-white:after,.divider.is-white:before{background-color:#fff}.divider.is-black:after,.divider.is-black:before{background-color:#0a0a0a}.divider.is-light:after,.divider.is-light:before{background-color:#f5f5f5}.divider.is-dark:after,.divider.is-dark:before{background-color:#363636}.divider.is-primary:after,.divider.is-primary:before{background-color:#00d1b2}.divider.is-primary.is-light:after,.divider.is-primary.is-light:before{background-color:#ebfffc}.divider.is-link:after,.divider.is-link:before{background-color:#3273dc}.divider.is-link.is-light:after,.divider.is-link.is-light:before{background-color:#eef3fc}.divider.is-info:after,.divider.is-info:before{background-color:#3298dc}.divider.is-info.is-light:after,.divider.is-info.is-light:before{background-color:#eef6fc}.divider.is-success:after,.divider.is-success:before{background-color:#48c774}.divider.is-success.is-light:after,.divider.is-success.is-light:before{background-color:#effaf3}.divider.is-warning:after,.divider.is-warning:before{background-color:#ffdd57}.divider.is-warning.is-light:after,.divider.is-warning.is-light:before{background-color:#fffbeb}.divider.is-danger:after,.divider.is-danger:before{background-color:#f14668}.divider.is-danger.is-light:after,.divider.is-danger.is-light:before{background-color:#feecf0}
|
||||
1
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-floating-button.min.css
vendored
Normal file
1
sources/services/web/project/static/styles/css/bulma/0.9.4/bulma-floating-button.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.button.is-floating{position:fixed;width:60px;height:60px;bottom:40px;right:40px;border-radius:100px;text-align:center;font-size:1.6rem;box-shadow:0 .0625em .125em rgba(10,10,10,.05);z-index:3}.button.is-floating.is-large{width:90px;height:90px;font-size:2.6rem}.button.is-floating.is-medium{width:75px;height:75px;font-size:2.2rem}.button.is-floating.is-small{width:45px;height:45px;font-size:1.2rem;border-radius:50px}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,52 @@
|
|||
.has-background-orange {
|
||||
background-color: #fddca4;
|
||||
}
|
||||
|
||||
.has-background-orange-2 {
|
||||
background-color: #f67a28;
|
||||
}
|
||||
|
||||
.has-background-gris {
|
||||
background-color: #dad7d7;
|
||||
}
|
||||
|
||||
img.profile-pic {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.is-abo-ok {
|
||||
background-color: #92d748;
|
||||
}
|
||||
|
||||
.is-abo-nok {
|
||||
background-color: #d74848;
|
||||
}
|
||||
|
||||
.has-background-violet-2 {
|
||||
background-color: #28235b;
|
||||
}
|
||||
|
||||
.has-background-violet {
|
||||
background-color: #62628c;
|
||||
}
|
||||
|
||||
.has-background-main-2 {
|
||||
background-color: #28235b;
|
||||
}
|
||||
|
||||
.has-background-main {
|
||||
background-color: #62628c;
|
||||
}
|
||||
|
||||
.has-border-color-main-2 {
|
||||
border-color: #28235b;
|
||||
background-color: #dad7d7;
|
||||
border-radius: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navbar.has-shadow-pum {
|
||||
box-shadow: 0 2px 0 0 #dad7d7;
|
||||
/*box-shadow: 0 2px 0 0 #28235b;*/
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
dzqdfs
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
{% extends "base.html" %}
|
||||
{#{% extends "base_login.html" %}#}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<input type="hidden" id="ongletType" value="none">
|
||||
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<div class="box has-background-main-2">
|
||||
<h1 class="title has-text-white">Board-Manager</h1>
|
||||
</div>
|
||||
<div class="box">
|
||||
|
||||
{% with success = get_flashed_messages(category_filter=["success"]) %}
|
||||
{% if success %}
|
||||
<div class="notification is-success">
|
||||
<button class="delete"></button>
|
||||
<p><strong>{{ success[0] }}</strong></p>
|
||||
{%- for msg in success[1:] %}
|
||||
<li>{{ msg }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with warn = get_flashed_messages(category_filter=["warn"]) %}
|
||||
{% if warn %}
|
||||
<div class="notification is-warning">
|
||||
<button class="delete"></button>
|
||||
<p><strong>{{ warn[0] }}</strong></p>
|
||||
{%- for msg in warn[1:] %}
|
||||
<li>{{ msg }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with error = get_flashed_messages(category_filter=["error"]) %}
|
||||
{% if error %}
|
||||
<div class="notification is-danger">
|
||||
<button class="delete"></button>
|
||||
<p><strong>{{ error[0] }}</strong></p>
|
||||
{%- for msg in error[1:] %}
|
||||
<li>{{ msg }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
|
||||
<div class="box message-content has-background-main">
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="log_type" value="classic" id="log_type"/>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-large" type="password" name="password" placeholder="Your Password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="rem-classic">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<button class="button is-block is-info is-large is-fullwidth">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{url_for('static', filename='scripts/js/jquery/jquery-3.6.0.min.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='scripts/js/auth/script.js')}}"></script>
|
||||
<script>
|
||||
|
||||
function OAuthSuccessCallback(authToken) {
|
||||
signIn(true, authToken);
|
||||
}
|
||||
function OAuthErrorCallback() {
|
||||
$('#sign-in-alert').text('Error communicating with Plex.tv.').show();
|
||||
}
|
||||
|
||||
$('#sign-in-plex').click(function() {
|
||||
console.log("Loggin")
|
||||
PlexOAuth(OAuthSuccessCallback, OAuthErrorCallback);
|
||||
});
|
||||
|
||||
function signIn(plex, token) {
|
||||
console.log(token)
|
||||
{#var rem_plex = 'caca'#}
|
||||
var rem_plex = document.getElementById('rem-plex').checked
|
||||
var form = $('<form action="/login" method="post">' + '<input type="hidden" name="log_type" value="plex" id="log_type"/>' + '<input type="hidden" name="rem-plex-jav" value=' + rem_plex + ' id="rem-plex-jav"/>' + '<input type="text" name="token" value="' + token + '" />' + '</form>');
|
||||
$('body').append(form);
|
||||
form.submit();
|
||||
{#window.location.href = "/login_plex" + token;#}
|
||||
{#$('#demo').load({{ url_for('auth.login_plex', share = token)}});#}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="column is-4 is-offset-4">
|
||||
<h3 class="title">Sign Up</h3>
|
||||
<div class="box">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="notification is-danger">
|
||||
{{ messages[0] }}<br> Go to <a href="{{ url_for('auth.login') }}">login page</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="/signup">
|
||||
<!--
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-large" type="email" name="email" placeholder="Email" autofocus="">
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-large" type="text" name="name" placeholder="Name" autofocus="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-large" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button is-block is-info is-large is-fullwidth">Sign Up</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Bulma Template</title>
|
||||
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-floating-button.min.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-divider.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma.min.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-collapsible.min.css') }}" />
|
||||
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/main.css') }}" />
|
||||
<!-- <link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/style.css') }}" />-->
|
||||
|
||||
<script src="{{url_for('static', filename='js/modal.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/bulma-collapsible.min.js')}}"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="hero" style="background-color:#DE926F;">
|
||||
|
||||
<div class="hero-head">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item">
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarMenuHeroA">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarMenuHeroA" class="navbar-menu" >
|
||||
<div class="navbar-end">
|
||||
<a href="{{ url_for('main.index') }}" class="navbar-item">
|
||||
Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hero is-fullheight" style="background-color:#DAD7D7;">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>PUM</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap">
|
||||
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-floating-button.min.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-divider.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma.min.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/bulma/0.9.4/bulma-collapsible.min.css') }}" />
|
||||
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/css/main.css') }}" />
|
||||
<link rel="stylesheet" type= "text/css" href="{{ url_for('static',filename='styles/style.css') }}" />
|
||||
|
||||
{# <link rel="icon" type="image/png" sizes="32x32" href="https://cdn.icon-icons.com/icons2/1465/PNG/512/095pileofpoo_100556.png" />#}
|
||||
{# <link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static',filename='images/shuffle-for-bulma.png') }}" />#}
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static',filename='images/sqyflix_logo.png') }}" />
|
||||
|
||||
<script src="{{url_for('static', filename='scripts/js/shuffle/js/main.js')}}"></script>
|
||||
|
||||
<script src="{{url_for('static', filename='scripts/js/ui/modal.js')}}"></script>
|
||||
<script type="text/javascript" src="{{url_for('static', filename='scripts/js/ui/pum.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='scripts/js/bulma/bulma-collapsible.min.js')}}"></script>
|
||||
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
{% include 'html_templates.html' %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section class="hero has-background-main">
|
||||
<div class="hero-head">
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item">
|
||||
<img src="{{ url_for('static',filename='images/sqyflix_logo.png') }}" alt="pum_logo" width="auto">
|
||||
{# <img src="{{ url_for('static',filename='images/pum_logo.svg') }}" alt="pum_logo" width="auto">#}
|
||||
</a>
|
||||
<div class="navbar-burger navbar-burger-old burger" data-target="navbarMenuHeroA">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarMenuHeroA" class="navbar-menu has-background-main" >
|
||||
<div class="navbar-end">
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a href="{{ url_for('auth.signup') }}" class="navbar-item has-text-white">
|
||||
Sign Up
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('auth.logout') }}" class="navbar-item has-text-white">
|
||||
Logout
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
<section class="hero is-fullheight" style="background-color:#DAD7D7;">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<div class="box">
|
||||
<h1 class="title">
|
||||
Bulma Template 02
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Flask==2.3.2
|
||||
Flask-SQLAlchemy==3.0.3
|
||||
gunicorn==20.1.0
|
||||
psycopg2-binary==2.9.6
|
||||
Flask-Login
|
||||
Loading…
Reference in New Issue