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