## First Commit ##

This commit is contained in:
ghatat 2024-09-06 16:51:25 +02:00
commit 2fc7242290
56 changed files with 3501 additions and 0 deletions

7
sources/.env.dev-sample Normal file
View File

@ -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

7
sources/.env.prod-sample Normal file
View File

@ -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

View File

@ -0,0 +1,3 @@
POSTGRES_USER=hello_flask
POSTGRES_PASSWORD=hello_flask
POSTGRES_DB=hello_flask_prod

6
sources/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pyc
__pycache
.DS_Store
.env.dev
.env.prod
.env.prod.db

21
sources/LICENSE Normal file
View File

@ -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.

35
sources/README.md Normal file
View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -0,0 +1,4 @@
FROM nginx:1.25
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

View File

@ -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/;
}
}

View File

@ -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"]

View File

@ -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"]

View File

@ -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"))

View File

@ -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 "$@"

View File

@ -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 "$@"

View File

@ -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()

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,2 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

Binary file not shown.

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from . import routes

View File

@ -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'))

View File

@ -0,0 +1,6 @@
from flask import Blueprint
bp = Blueprint('main', __name__)
from . import routes
# from services.web.project.routes.main import routes

View File

@ -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>'

View File

@ -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

View File

@ -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

View File

@ -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>&nbsp;' +
'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>&nbsp; " + 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>&nbsp; ' + msg, false, true, 5000);
} else {
showMsg('<i class="fa fa-times"></i>&nbsp; ' + 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>&nbsp; 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>&nbsp; " + dataSucces + "</div>");
var errorMsg = $("<div class='msg'><i class='fa fa-exclamation-triangle'></i>&nbsp; " + 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) + '&nbsp;' + 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;
}

View 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}

View 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}

View File

@ -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;
}

View 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}

View 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

View File

@ -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

View File

@ -0,0 +1 @@
dzqdfs

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -0,0 +1,5 @@
Flask==2.3.2
Flask-SQLAlchemy==3.0.3
gunicorn==20.1.0
psycopg2-binary==2.9.6
Flask-Login