diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f5d25f..1c192dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,6 @@ "noscript", "stylesheet", "tauth", - "TCOMMON" + "tcommon" ] } diff --git a/tauth/migrations/0002_tsession.py b/tauth/migrations/0002_tsession.py new file mode 100644 index 0000000..0207558 --- /dev/null +++ b/tauth/migrations/0002_tsession.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.7 on 2024-12-24 02:08 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tauth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='tSession', + fields=[ + ('session_id', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ('last_use', models.IntegerField()), + ('created', models.IntegerField()), + ('u_for', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tauth/models.py b/tauth/models.py index fc1cb2a..998fd61 100644 --- a/tauth/models.py +++ b/tauth/models.py @@ -1,6 +1,36 @@ +import math +import time +from typing import TYPE_CHECKING + +from django.contrib import admin as django_admin +from django.contrib.admin.exceptions import AlreadyRegistered # type: ignore from django.contrib.auth.models import User from django.db import models class tUser(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) + + if TYPE_CHECKING: + sessions = models.Manager["tSession"] + + def __str__(self) -> str: + return self.user.username + +class tSession(models.Model): + u_for = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sessions") + session_id = models.CharField(max_length=128, unique=True, primary_key=True) + last_use = models.IntegerField() + created = models.IntegerField() + + def register_usage(self): + self.last_use = math.floor(time.time()) + self.save(update_fields=["last_use"]) + + def __str__(self) -> str: + return f"{self.u_for.username} - created: {self.created} - last usage: {self.last_use}" + +try: + django_admin.site.register((tUser, tSession)) +except AlreadyRegistered: + ... diff --git a/tauth/sessions.py b/tauth/sessions.py new file mode 100644 index 0000000..1398b58 --- /dev/null +++ b/tauth/sessions.py @@ -0,0 +1,99 @@ +import hashlib +import math +import random +import threading +import time +from datetime import datetime + +from django.contrib.auth.models import AbstractUser, User +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse + +from tauth.models import tSession +from tauth.settings import config + +_last_trim = 0 + +def _get_session_id(request: WSGIRequest, /, username: str) -> str: + return hashlib.sha3_512(str.encode(f"{request.META['HTTP_USER_AGENT']}-{time.time()}-{random.random()}-{username}")).hexdigest() + +def _trim_sessions(): + if time.time() < _last_trim + 60 * 60: + return + + thread = threading.Thread(target=__th_trim_sessions) + thread.start() + +def __th_trim_sessions(): + global _last_trim + _last_trim = time.time() + + tSession.objects.filter( + last_use__lt=math.floor(time.time()) - config["session_timeout"] * 60 * 60 + ).delete() + + if config["session_length"] != -1: + tSession.objects.filter( + created__lt=math.floor(time.time()) - config["session_length"] * 60 * 60 + ).delete() + +def create_session(request: WSGIRequest, response: HttpResponse | None, /, user: User | AbstractUser) -> tSession: + _trim_sessions() + session = tSession.objects.create( + u_for=user, + session_id=_get_session_id(request, user.username), + created=math.floor(time.time()), + last_use=math.floor(time.time()) + ) + + if response is not None: + response.set_cookie("session_id", session.session_id, max_age=365 * 24 * 60 * 60 if config["session_length"] == -1 else (config["session_length"] * 60 * 60)) + + return session + +def clear_session(request: WSGIRequest, response: HttpResponse | None=None, /, delete_session: bool=True): + _trim_sessions() + session_id = request.COOKIES.get("session_id") + + if session_id is None or len(session_id) != 128: + return + + if response is not None: + response.set_cookie("session_id", "", max_age=0, expires=datetime(0, 0, 0)) + + try: + session = tSession.objects.get(session_id=session_id) + except tSession.DoesNotExist: + ... + + if delete_session: + session.delete() + +def get_user(request: WSGIRequest, /) -> User | None: + _trim_sessions() + session_id = request.COOKIES.get("session_id") + + if session_id is None or len(session_id) != 128: + return None + + try: + session = tSession.objects.get(session_id=session_id) + except tSession.DoesNotExist: + return None + + session.register_usage() + return session.u_for + +def is_authenticated(request: WSGIRequest, /) -> bool: + _trim_sessions() + session_id = request.COOKIES.get("session_id") + + if session_id is None or len(session_id) != 128: + return False + + try: + tSession.objects.get(session_id=session_id).register_usage() + except tSession.DoesNotExist: + return False + + return True diff --git a/tauth/templates/noauth/index.html b/tauth/templates/noauth/index.html index 0a23e19..3aff433 100644 --- a/tauth/templates/noauth/index.html +++ b/tauth/templates/noauth/index.html @@ -6,9 +6,9 @@
(assuming you don't need much)

Log in - {% if new_users %} - Sign up{% endif %} + {% if config.new_users %} - Sign up{% endif %}

- {% if not new_users %} + {% if not config.new_users %}
This instance isn't accepting new users.
{% endif %} {% endblock %} diff --git a/tauth/views/api.py b/tauth/views/api.py index 2f48f77..9183810 100644 --- a/tauth/views/api.py +++ b/tauth/views/api.py @@ -4,18 +4,19 @@ from django.contrib.auth.models import User from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponse +from tauth.sessions import get_user from tauth.settings import config def get_username(request: WSGIRequest) -> HttpResponse: try: if request.GET.get("token") == config["services"][request.GET.get("service")]["token"]: - authenticated = request.user.is_authenticated + user = get_user(request) return HttpResponse( json.dumps({ "success": True, - "authenticated": authenticated, - "username": request.user.get_username() if authenticated else None + "authenticated": user is not None, + "username": user and user.username }), content_type="application/json" ) diff --git a/tauth/views/helper.py b/tauth/views/helper.py index 17f8b45..aef27ab 100644 --- a/tauth/views/helper.py +++ b/tauth/views/helper.py @@ -20,7 +20,7 @@ def render_template( c = { "accent": random.choice(COLORS), "config": config, - "login_token": f"/auth/?sessionid={request.COOKIES.get('sessionid')}" + "login_token": f"/auth/?sessionid={request.COOKIES.get('session_id')}" } for key, val in context.items(): diff --git a/tauth/views/templates.py b/tauth/views/templates.py index 18c45bd..e5499cc 100644 --- a/tauth/views/templates.py +++ b/tauth/views/templates.py @@ -2,13 +2,13 @@ import re from datetime import datetime from django.contrib.auth import authenticate -from django.contrib.auth import login as set_auth -from django.contrib.auth import logout as remove_auth from django.contrib.auth.models import User from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponse, HttpResponseRedirect from tauth.models import tUser +from tauth.sessions import (clear_session, create_session, get_user, + is_authenticated) from tauth.settings import config from .helper import render_template @@ -17,26 +17,26 @@ from .helper import render_template def auth(request: WSGIRequest) -> HttpResponseRedirect: resp = HttpResponseRedirect("/") if "remove" in request.GET: - resp.set_cookie("sessionid", "", max_age=0, expires=datetime(0, 0, 0)) + resp.set_cookie("session_id", "", max_age=0, expires=datetime(0, 0, 0)) else: - resp.set_cookie("sessionid", request.GET.get("sessionid") or "") + resp.set_cookie("session_id", request.GET.get("sessionid") or "") return resp def index(request: WSGIRequest) -> HttpResponse: - if request.user.is_authenticated: + u = get_user(request) + if u: return render_template( request, "index.html", - username=request.user.get_username() + username=u.username ) return render_template( - request, "noauth/index.html", - new_users=config["new_users"] + request, "noauth/index.html" ) def signup(request: WSGIRequest) -> HttpResponse: - if request.user.is_authenticated: + if is_authenticated(request): return HttpResponseRedirect("/") if config["new_users"]: @@ -64,13 +64,15 @@ def signup(request: WSGIRequest) -> HttpResponse: error = "Username already in use" else: tUser.objects.create(user=u) - set_auth(request, u) to = request.GET.get("to") if to and to in config["services"] and config["services"][to]: - return HttpResponseRedirect(f"/redirect/?to={to}&reauth") + resp = HttpResponseRedirect(f"/redirect/?to={to}&reauth") + else: + resp = HttpResponseRedirect("/") - return HttpResponseRedirect("/") + create_session(request, resp, u) + return resp return render_template( request, "noauth/signup.html", @@ -93,10 +95,10 @@ def signup(request: WSGIRequest) -> HttpResponse: ) def login(request: WSGIRequest) -> HttpResponse: - if request.user.is_authenticated: + if is_authenticated(request): to = request.GET.get("to") - if to and to in config["services"] and config["services"]["to"]: + if to and to in config["services"] and config["services"][to]: return HttpResponseRedirect(f"/redirect/?to={to}&reauth") return HttpResponseRedirect("/") @@ -123,13 +125,15 @@ def login(request: WSGIRequest) -> HttpResponse: } ) - set_auth(request, user) to = request.GET.get("to") if to and to in config["services"] and config["services"][to]: - return HttpResponseRedirect(f"/redirect/?to={to}&reauth") + resp = HttpResponseRedirect(f"/redirect/?to={to}&reauth") + else: + resp = HttpResponseRedirect("/") - return HttpResponseRedirect("/") + create_session(request, resp, user) + return resp return render_template( @@ -143,12 +147,12 @@ def redirect(request: WSGIRequest) -> HttpResponseRedirect: to = request.GET.get("to") if to and to in config["services"] and config["services"][to]: - return HttpResponseRedirect(config["services"][to]["url"]["pub"] + (f"/auth/?sessionid={request.COOKIES.get('sessionid')}" if "reauth" in request.GET else "")) + return HttpResponseRedirect(config["services"][to]["url"]["pub"] + (f"/auth/?sessionid={request.COOKIES.get('session_id')}" if "reauth" in request.GET else "")) return HttpResponseRedirect("/") def logout(request: WSGIRequest) -> HttpResponseRedirect: - remove_auth(request) + clear_session(request) to = request.GET.get("to") if to and to in config["services"] and config["services"][to]: