custom session manager
This commit is contained in:
parent
6f6ff87a85
commit
08131fbc18
8 changed files with 185 additions and 26 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -4,6 +4,6 @@
|
||||||
"noscript",
|
"noscript",
|
||||||
"stylesheet",
|
"stylesheet",
|
||||||
"tauth",
|
"tauth",
|
||||||
"TCOMMON"
|
"tcommon"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
25
tauth/migrations/0002_tsession.py
Normal file
25
tauth/migrations/0002_tsession.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
99
tauth/sessions.py
Normal 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
|
|
@ -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 %}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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]:
|
||||||
|
|
Loading…
Reference in a new issue