custom session manager

This commit is contained in:
trinkey 2024-12-23 22:38:56 -05:00
parent 6f6ff87a85
commit 08131fbc18
8 changed files with 185 additions and 26 deletions

View file

@ -4,6 +4,6 @@
"noscript", "noscript",
"stylesheet", "stylesheet",
"tauth", "tauth",
"TCOMMON" "tcommon"
] ]
} }

View file

@ -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)),
],
),
]

View file

@ -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.contrib.auth.models import User
from django.db import models from django.db import models
class tUser(models.Model): class tUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) 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:
...

99
tauth/sessions.py Normal file
View file

@ -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

View file

@ -6,9 +6,9 @@
<div><small>(assuming you don't need much)</small></div> <div><small>(assuming you don't need much)</small></div>
<p> <p>
<a href="/login/">Log in</a> <a href="/login/">Log in</a>
{% if new_users %} - <a href="/signup/">Sign up</a>{% endif %} {% if config.new_users %} - <a href="/signup/">Sign up</a>{% endif %}
</p> </p>
{% if not new_users %} {% if not config.new_users %}
<div>This instance isn't accepting new users.</div> <div>This instance isn't accepting new users.</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -4,18 +4,19 @@ from django.contrib.auth.models import User
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse from django.http import HttpResponse
from tauth.sessions import get_user
from tauth.settings import config from tauth.settings import config
def get_username(request: WSGIRequest) -> HttpResponse: def get_username(request: WSGIRequest) -> HttpResponse:
try: try:
if request.GET.get("token") == config["services"][request.GET.get("service")]["token"]: if request.GET.get("token") == config["services"][request.GET.get("service")]["token"]:
authenticated = request.user.is_authenticated user = get_user(request)
return HttpResponse( return HttpResponse(
json.dumps({ json.dumps({
"success": True, "success": True,
"authenticated": authenticated, "authenticated": user is not None,
"username": request.user.get_username() if authenticated else None "username": user and user.username
}), }),
content_type="application/json" content_type="application/json"
) )

View file

@ -20,7 +20,7 @@ def render_template(
c = { c = {
"accent": random.choice(COLORS), "accent": random.choice(COLORS),
"config": config, "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(): for key, val in context.items():

View file

@ -2,13 +2,13 @@ import re
from datetime import datetime from datetime import datetime
from django.contrib.auth import authenticate 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.contrib.auth.models import User
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from tauth.models import tUser from tauth.models import tUser
from tauth.sessions import (clear_session, create_session, get_user,
is_authenticated)
from tauth.settings import config from tauth.settings import config
from .helper import render_template from .helper import render_template
@ -17,26 +17,26 @@ from .helper import render_template
def auth(request: WSGIRequest) -> HttpResponseRedirect: def auth(request: WSGIRequest) -> HttpResponseRedirect:
resp = HttpResponseRedirect("/") resp = HttpResponseRedirect("/")
if "remove" in request.GET: 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: else:
resp.set_cookie("sessionid", request.GET.get("sessionid") or "") resp.set_cookie("session_id", request.GET.get("sessionid") or "")
return resp return resp
def index(request: WSGIRequest) -> HttpResponse: def index(request: WSGIRequest) -> HttpResponse:
if request.user.is_authenticated: u = get_user(request)
if u:
return render_template( return render_template(
request, "index.html", request, "index.html",
username=request.user.get_username() username=u.username
) )
return render_template( return render_template(
request, "noauth/index.html", request, "noauth/index.html"
new_users=config["new_users"]
) )
def signup(request: WSGIRequest) -> HttpResponse: def signup(request: WSGIRequest) -> HttpResponse:
if request.user.is_authenticated: if is_authenticated(request):
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
if config["new_users"]: if config["new_users"]:
@ -64,13 +64,15 @@ def signup(request: WSGIRequest) -> HttpResponse:
error = "Username already in use" error = "Username already in use"
else: else:
tUser.objects.create(user=u) tUser.objects.create(user=u)
set_auth(request, u)
to = request.GET.get("to") 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") resp = HttpResponseRedirect(f"/redirect/?to={to}&reauth")
else:
resp = HttpResponseRedirect("/")
return HttpResponseRedirect("/") create_session(request, resp, u)
return resp
return render_template( return render_template(
request, "noauth/signup.html", request, "noauth/signup.html",
@ -93,10 +95,10 @@ def signup(request: WSGIRequest) -> HttpResponse:
) )
def login(request: WSGIRequest) -> HttpResponse: def login(request: WSGIRequest) -> HttpResponse:
if request.user.is_authenticated: if is_authenticated(request):
to = request.GET.get("to") 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(f"/redirect/?to={to}&reauth")
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
@ -123,13 +125,15 @@ def login(request: WSGIRequest) -> HttpResponse:
} }
) )
set_auth(request, user)
to = request.GET.get("to") 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") resp = HttpResponseRedirect(f"/redirect/?to={to}&reauth")
else:
resp = HttpResponseRedirect("/")
return HttpResponseRedirect("/") create_session(request, resp, user)
return resp
return render_template( return render_template(
@ -143,12 +147,12 @@ def redirect(request: WSGIRequest) -> HttpResponseRedirect:
to = request.GET.get("to") 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(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("/") return HttpResponseRedirect("/")
def logout(request: WSGIRequest) -> HttpResponseRedirect: def logout(request: WSGIRequest) -> HttpResponseRedirect:
remove_auth(request) clear_session(request)
to = request.GET.get("to") 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]: