implement login + logout routes and templates

+ Added route: GET `/login` via `aurweb.routers.auth.login_get`
+ Added route: POST `/login` via `aurweb.routers.auth.login_post`
+ Added route: GET `/logout` via `aurweb.routers.auth.logout`
+ Added route: POST `/logout` via `aurweb.routers.auth.logout_post`
* Modify archdev-navbar.html template to toggle displays on auth state
+ Added login.html template

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2020-12-31 20:44:59 -08:00
parent 56f2798279
commit 5d4a5deddf
5 changed files with 313 additions and 3 deletions

View file

@ -11,7 +11,7 @@ import aurweb.config
from aurweb.auth import BasicAuthBackend
from aurweb.db import get_engine
from aurweb.routers import html, sso, errors
from aurweb.routers import auth, html, sso, errors
routes = set()
@ -42,6 +42,7 @@ async def app_startup():
# Add application routes.
app.include_router(sso.router)
app.include_router(html.router)
app.include_router(auth.router)
# Initialize the database engine and ORM.
get_engine()

85
aurweb/routers/auth.py Normal file
View file

@ -0,0 +1,85 @@
from datetime import datetime
from http import HTTPStatus
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
import aurweb.config
from aurweb.models.user import User
from aurweb.templates import make_context, render_template
router = APIRouter()
def login_template(request: Request, next: str, errors: list = None):
""" Provide login-specific template context to render_template. """
context = make_context(request, "Login", next)
context["errors"] = errors
context["url_base"] = f"{request.url.scheme}://{request.url.netloc}"
return render_template("login.html", context)
@router.get("/login", response_class=HTMLResponse)
async def login_get(request: Request, next: str = "/"):
""" Homepage route. """
return login_template(request, next)
@router.post("/login", response_class=HTMLResponse)
async def login_post(request: Request,
next: str = Form(...),
user: str = Form(default=str()),
passwd: str = Form(default=str()),
remember_me: bool = Form(default=False)):
from aurweb.db import session
user = session.query(User).filter(User.Username == user).first()
if not user:
return login_template(request, next,
errors=["Bad username or password."])
cookie_timeout = 0
if remember_me:
cookie_timeout = aurweb.config.getint(
"options", "persistent_cookie_timeout")
_, sid = user.login(request, passwd, cookie_timeout)
if not _:
return login_template(request, next,
errors=["Bad username or password."])
login_timeout = aurweb.config.getint("options", "login_timeout")
expires_at = int(datetime.utcnow().timestamp()
+ max(cookie_timeout, login_timeout))
response = RedirectResponse(url=next,
status_code=int(HTTPStatus.SEE_OTHER))
response.set_cookie("AURSID", sid, expires=expires_at)
return response
@router.get("/logout")
async def logout(request: Request, next: str = "/"):
""" A GET and POST route for logging out.
@param request FastAPI request
@param next Route to redirect to
"""
if request.user.is_authenticated():
request.user.logout(request)
# Use 303 since we may be handling a post request, that'll get it
# to redirect to a get request.
response = RedirectResponse(url=next,
status_code=int(HTTPStatus.SEE_OTHER))
response.delete_cookie("AURSID")
response.delete_cookie("AURTZ")
return response
@router.post("/logout")
async def logout_post(request: Request, next: str = "/"):
return await logout(request=request, next=next)

84
templates/login.html Normal file
View file

@ -0,0 +1,84 @@
{% extends 'partials/layout.html' %}
{% block pageContent %}
<div id="dev-login" class="box">
<h2>AUR {% trans %}Login{% endtrans %}</h2>
{% if request.url.scheme == "http" and config.getboolean("options", "disable_http_login") %}
{% set https_login = url_base.replace("http://", "https://") + "/login/" %}
<p>
{{ "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login."
| tr
| format(
'<a href="%s">' | format(https_login),
"</a>")
| safe
}}
</p>
{% else %}
{% if request.user.is_authenticated() %}
<p>
{{ "Logged-in as: %s"
| tr
| format("<b>%s</b>" | format(request.user.Username))
| safe
}}
<a href="/logout/?next={{ next }}">[{% trans %}Logout{% endtrans %}]</a>
</p>
{% else %}
<form method="post" action="/login?next={{ next }}">
<fieldset>
<legend>{% trans %}Enter login credentials{% endtrans %}</legend>
{% if errors %}
<ul class="errorlist">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<p>
<label for="id_username">
{% trans %}User name or primary email address{% endtrans %}:
</label>
<input id="id_username" type="text" name="user" size="30"
maxlength="254" autofocus="autofocus">
</p>
<p>
<label for="id_password">
{% trans %}Password{% endtrans %}:
</label>
<input id="id_password" type="password" name="passwd"
size="30">
</p>
<p>
<input id="id_remember_me" type="checkbox" name="remember_me">
<label for="id_remember_me">
{% trans %}Remember me{% endtrans %}
</label>
</p>
<p>
<input class="button" type="submit"
value="{% trans %}Login{% endtrans %}">
<a href="/passreset/">
[{% trans %}Forgot Password{% endtrans %}]
</a>
<input id="id_referer" type="hidden" name="referer"
value="{{ url_base }}">
<input type="hidden" name="next" value="{{ next }}">
</p>
</fieldset>
</form>
{% endif %}
{% endif %}
</div>
{% endblock %}

View file

@ -1,8 +1,22 @@
<div id="archdev-navbar">
<ul>
<li><a href="/">AUR {% trans %}Home{% endtrans %}</a></li>
{% if request.user.is_authenticated() %}
<li><a href="/">{% trans %}Dashboard{% endtrans %}</a></li>
{% else %}
<li><a href="/">AUR {% trans %}Home{% endtrans %}</a></li>
{% endif %}
<li><a href="/packages/">{% trans %}Packages{% endtrans %}</a></li>
<li><a href="/register/">{% trans %}Register{% endtrans %}</a></li>
<li><a href="/login/">{% trans %}Login{% endtrans %}</a></li>
<li>
{% if request.user.is_authenticated() %}
<a href="/logout/?next={{ next }}">
{% trans %}Logout{% endtrans %}
</a>
{% else %}
<a href="/login/?next={{ next }}">
{% trans %}Login{% endtrans %}
</a>
{% endif %}
</li>
</ul>
</div>

126
test/test_auth_routes.py Normal file
View file

@ -0,0 +1,126 @@
from datetime import datetime
from http import HTTPStatus
import pytest
from fastapi.testclient import TestClient
import aurweb.config
from aurweb.asgi import app
from aurweb.db import query
from aurweb.models.account_type import AccountType
from aurweb.models.session import Session
from aurweb.testing import setup_test_db
from aurweb.testing.models import make_user
client = TestClient(app)
user = None
@pytest.fixture(autouse=True)
def setup():
global user
setup_test_db("Users", "Sessions", "Bans")
account_type = query(AccountType,
AccountType.AccountType == "User").first()
user = make_user(Username="test", Email="test@example.org",
RealName="Test User", Passwd="testPassword",
AccountType=account_type)
def test_login_logout():
post_data = {
"user": "test",
"passwd": "testPassword",
"next": "/"
}
with client as request:
response = client.get("/login")
assert response.status_code == int(HTTPStatus.OK)
response = request.post("/login", data=post_data,
allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
response = request.get(response.headers.get("location"), cookies={
"AURSID": response.cookies.get("AURSID")
})
assert response.status_code == int(HTTPStatus.OK)
response = request.post("/logout", data=post_data,
allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
response = request.post("/logout", data=post_data, cookies={
"AURSID": response.cookies.get("AURSID")
}, allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
assert "AURSID" not in response.cookies
def test_login_missing_username():
post_data = {
"passwd": "testPassword",
"next": "/"
}
with client as request:
response = request.post("/login", data=post_data)
assert "AURSID" not in response.cookies
def test_login_remember_me():
from aurweb.db import session
post_data = {
"user": "test",
"passwd": "testPassword",
"next": "/",
"remember_me": True
}
with client as request:
response = request.post("/login", data=post_data,
allow_redirects=False)
assert response.status_code == int(HTTPStatus.SEE_OTHER)
assert "AURSID" in response.cookies
cookie_timeout = aurweb.config.getint(
"options", "persistent_cookie_timeout")
expected_ts = datetime.utcnow().timestamp() + cookie_timeout
_session = session.query(Session).filter(
Session.UsersID == user.ID).first()
# Expect that LastUpdateTS was within 5 seconds of the expected_ts,
# which is equal to the current timestamp + persistent_cookie_timeout.
assert _session.LastUpdateTS > expected_ts - 5
assert _session.LastUpdateTS < expected_ts + 5
def test_login_missing_password():
post_data = {
"user": "test",
"next": "/"
}
with client as request:
response = request.post("/login", data=post_data)
assert "AURSID" not in response.cookies
def test_login_incorrect_password():
post_data = {
"user": "test",
"passwd": "badPassword",
"next": "/"
}
with client as request:
response = request.post("/login", data=post_data)
assert "AURSID" not in response.cookies