2023-04-04 18:17:44 +08:00
|
|
|
# The Mia! Accounting Demonstration Website.
|
2023-01-29 22:28:27 +08:00
|
|
|
# Author: imacat@mail.imacat.idv.tw (imacat), 2023/1/27
|
|
|
|
|
|
|
|
# Copyright (c) 2023 imacat.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
2023-04-04 18:17:44 +08:00
|
|
|
"""The authentication for the Mia! Accounting demonstration website.
|
2023-01-29 22:28:27 +08:00
|
|
|
|
|
|
|
"""
|
2023-04-13 09:39:52 +08:00
|
|
|
import typing as t
|
|
|
|
|
2023-01-29 22:28:27 +08:00
|
|
|
from flask import Blueprint, render_template, Flask, redirect, url_for, \
|
2023-04-13 09:39:52 +08:00
|
|
|
session, request, g, Response, abort
|
2023-04-24 14:02:56 +08:00
|
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
2023-01-29 22:28:27 +08:00
|
|
|
|
|
|
|
from . import db
|
|
|
|
|
|
|
|
bp: Blueprint = Blueprint("auth", __name__, url_prefix="/")
|
2023-04-24 14:00:32 +08:00
|
|
|
"""The authentication blueprint."""
|
2023-01-29 22:28:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
class User(db.Model):
|
|
|
|
"""A user."""
|
|
|
|
__tablename__ = "users"
|
|
|
|
"""The table name."""
|
2023-04-24 14:02:56 +08:00
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
|
|
"""The ID."""
|
|
|
|
username: Mapped[str] = mapped_column(unique=True)
|
2023-01-29 22:28:27 +08:00
|
|
|
"""The username."""
|
|
|
|
|
2023-02-01 15:37:56 +08:00
|
|
|
def __str__(self) -> str:
|
|
|
|
"""Returns the string representation of the user.
|
|
|
|
|
|
|
|
:return: The string representation of the user.
|
|
|
|
"""
|
|
|
|
return self.username
|
|
|
|
|
2023-01-29 22:28:27 +08:00
|
|
|
|
|
|
|
@bp.get("login", endpoint="login-form")
|
2023-04-11 22:05:57 +08:00
|
|
|
def show_login_form() -> str | Response:
|
2023-01-29 22:28:27 +08:00
|
|
|
"""Shows the login form.
|
|
|
|
|
|
|
|
:return: The login form.
|
|
|
|
"""
|
2023-04-11 22:05:57 +08:00
|
|
|
if "user" in session:
|
|
|
|
return redirect(url_for("accounting-report.default"))
|
2023-01-29 22:28:27 +08:00
|
|
|
return render_template("login.html")
|
|
|
|
|
|
|
|
|
|
|
|
@bp.post("login", endpoint="login")
|
|
|
|
def login() -> redirect:
|
|
|
|
"""Logs in the user.
|
|
|
|
|
|
|
|
:return: The redirection to the home page.
|
|
|
|
"""
|
2023-04-11 22:05:57 +08:00
|
|
|
from accounting.utils.next_uri import inherit_next, or_next
|
2023-03-24 08:32:28 +08:00
|
|
|
if request.form.get("username") not in {"viewer", "editor", "admin",
|
|
|
|
"nobody"}:
|
2023-04-11 22:05:57 +08:00
|
|
|
return redirect(inherit_next(url_for("auth.login")))
|
2023-01-29 22:28:27 +08:00
|
|
|
session["user"] = request.form.get("username")
|
2023-04-11 22:05:57 +08:00
|
|
|
return redirect(or_next(url_for("accounting-report.default")))
|
2023-01-29 22:28:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
@bp.post("logout", endpoint="logout")
|
|
|
|
def logout() -> redirect:
|
|
|
|
"""Logs out the user.
|
|
|
|
|
|
|
|
:return: The redirection to the home page.
|
|
|
|
"""
|
|
|
|
if "user" in session:
|
|
|
|
del session["user"]
|
|
|
|
return redirect(url_for("home.home"))
|
|
|
|
|
|
|
|
|
|
|
|
def current_user() -> User | None:
|
|
|
|
"""Returns the current user.
|
|
|
|
|
|
|
|
:return: The current user, or None if the user did not log in.
|
|
|
|
"""
|
|
|
|
if not hasattr(g, "user"):
|
|
|
|
if "user" not in session:
|
|
|
|
g.user = None
|
|
|
|
else:
|
|
|
|
g.user = User.query.filter(
|
|
|
|
User.username == session["user"]).first()
|
|
|
|
return g.user
|
|
|
|
|
|
|
|
|
2023-04-13 09:39:52 +08:00
|
|
|
def admin_required(view: t.Callable) -> t.Callable:
|
|
|
|
"""The view decorator to require the user to be an administrator.
|
|
|
|
|
|
|
|
:param view: The view.
|
|
|
|
:return: The decorated view.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def decorated_view(*args, **kwargs):
|
|
|
|
"""The decorated view that tests against a permission rule.
|
|
|
|
|
|
|
|
:param args: The arguments of the view.
|
|
|
|
:param kwargs: The keyword arguments of the view.
|
|
|
|
:return: The response of the view.
|
|
|
|
:raise Forbidden: When the user is denied.
|
|
|
|
"""
|
|
|
|
from accounting.utils.next_uri import append_next
|
|
|
|
if "user" not in session:
|
|
|
|
return redirect(append_next(url_for("auth.login")))
|
|
|
|
if session["user"] != "admin":
|
|
|
|
abort(403)
|
|
|
|
return view(*args, **kwargs)
|
|
|
|
|
|
|
|
return decorated_view
|
|
|
|
|
|
|
|
|
2023-01-29 22:28:27 +08:00
|
|
|
def init_app(app: Flask) -> None:
|
|
|
|
"""Initialize the localization.
|
|
|
|
|
|
|
|
:param app: The Flask application.
|
|
|
|
:return: None.
|
|
|
|
"""
|
|
|
|
app.register_blueprint(bp)
|
|
|
|
app.jinja_env.globals["current_user"] = current_user
|