mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
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:
parent
56f2798279
commit
5d4a5deddf
5 changed files with 313 additions and 3 deletions
|
@ -11,7 +11,7 @@ import aurweb.config
|
||||||
|
|
||||||
from aurweb.auth import BasicAuthBackend
|
from aurweb.auth import BasicAuthBackend
|
||||||
from aurweb.db import get_engine
|
from aurweb.db import get_engine
|
||||||
from aurweb.routers import html, sso, errors
|
from aurweb.routers import auth, html, sso, errors
|
||||||
|
|
||||||
routes = set()
|
routes = set()
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ async def app_startup():
|
||||||
# Add application routes.
|
# Add application routes.
|
||||||
app.include_router(sso.router)
|
app.include_router(sso.router)
|
||||||
app.include_router(html.router)
|
app.include_router(html.router)
|
||||||
|
app.include_router(auth.router)
|
||||||
|
|
||||||
# Initialize the database engine and ORM.
|
# Initialize the database engine and ORM.
|
||||||
get_engine()
|
get_engine()
|
||||||
|
|
85
aurweb/routers/auth.py
Normal file
85
aurweb/routers/auth.py
Normal 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
84
templates/login.html
Normal 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 %}
|
|
@ -1,8 +1,22 @@
|
||||||
<div id="archdev-navbar">
|
<div id="archdev-navbar">
|
||||||
<ul>
|
<ul>
|
||||||
|
{% if request.user.is_authenticated() %}
|
||||||
|
<li><a href="/">{% trans %}Dashboard{% endtrans %}</a></li>
|
||||||
|
{% else %}
|
||||||
<li><a href="/">AUR {% trans %}Home{% endtrans %}</a></li>
|
<li><a href="/">AUR {% trans %}Home{% endtrans %}</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><a href="/packages/">{% trans %}Packages{% endtrans %}</a></li>
|
<li><a href="/packages/">{% trans %}Packages{% endtrans %}</a></li>
|
||||||
<li><a href="/register/">{% trans %}Register{% 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
126
test/test_auth_routes.py
Normal file
126
test/test_auth_routes.py
Normal 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
|
Loading…
Add table
Reference in a new issue