frontend: fix login, remove frontend_session in favor of twisted sessions

This commit is contained in:
Hay1tsme 2023-03-03 21:31:23 -05:00
parent dc5e5c1440
commit 279f48dc0c
7 changed files with 88 additions and 71 deletions

View File

@ -31,7 +31,7 @@ class Data:
self.arcade = ArcadeData(self.config, self.session) self.arcade = ArcadeData(self.config, self.session)
self.card = CardData(self.config, self.session) self.card = CardData(self.config, self.session)
self.base = BaseData(self.config, self.session) self.base = BaseData(self.config, self.session)
self.schema_ver_latest = 2 self.schema_ver_latest = 4
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s" log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str) log_fmt = logging.Formatter(log_fmt_str)

View File

@ -9,6 +9,7 @@ from sqlalchemy.sql import func, select, Delete
from uuid import uuid4 from uuid import uuid4
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
import bcrypt
from core.data.schema.base import BaseData, metadata from core.data.schema.base import BaseData, metadata
@ -26,17 +27,6 @@ aime_user = Table(
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )
frontend_session = Table(
"frontend_session",
metadata,
Column("id", Integer, primary_key=True, unique=True),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("ip", String(15)),
Column('session_cookie', String(32), nullable=False, unique=True),
Column("expires", TIMESTAMP, nullable=False),
mysql_charset='utf8mb4'
)
class PermissionBits(Enum): class PermissionBits(Enum):
PermUser = 1 PermUser = 1
PermMod = 2 PermMod = 2
@ -74,50 +64,20 @@ class UserData(BaseData):
if result is None: return None if result is None: return None
return result.lastrowid return result.lastrowid
def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]: def get_user(self, user_id: int) -> Optional[Row]:
sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd)) sql = select(aime_user).where(aime_user.c.id == user_id)
result = self.execute(sql)
if result is None: return None
usr = result.fetchone()
if usr is None: return None
return self.create_session(user_id, ip)
def check_session(self, cookie: str, ip: str = "0.0.0.0") -> Optional[Row]:
sql = select(frontend_session).where(
and_(
frontend_session.c.session_cookie == cookie,
frontend_session.c.ip == ip
)
)
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def delete_session(self, session_id: int) -> bool:
sql = Delete(frontend_session).where(frontend_session.c.id == session_id)
result = self.execute(sql) result = self.execute(sql)
if result is None: return False if result is None: return False
return True return result.fetchone()
def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]: def check_password(self, user_id: int, passwd: bytes = None) -> bool:
cookie = uuid4().hex usr = self.get_user(user_id)
if usr is None: return False
sql = insert(frontend_session).values( if usr['password'] is None:
user = user_id, return False
ip = ip,
session_cookie = cookie,
expires = expires
)
result = self.execute(sql) return bcrypt.checkpw(passwd, usr['password'].encode())
if result is None:
return None
return cookie
def reset_autoincrement(self, ai_value: int) -> None: def reset_autoincrement(self, ai_value: int) -> None:
# ALTER TABLE isn't in sqlalchemy so we do this the ugly way # ALTER TABLE isn't in sqlalchemy so we do this the ugly way

View File

@ -0,0 +1,12 @@
CREATE TABLE `frontend_session` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` int(11) NOT NULL,
`ip` varchar(15) DEFAULT NULL,
`session_cookie` varchar(32) NOT NULL,
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
UNIQUE KEY `session_cookie` (`session_cookie`),
KEY `user` (`user`),
CONSTRAINT `frontend_session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1 @@
DROP TABLE `frontend_session`;

View File

@ -4,6 +4,9 @@ from twisted.web import resource
from twisted.web.util import redirectTo from twisted.web.util import redirectTo
from twisted.web.http import Request from twisted.web.http import Request
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from twisted.web.server import Session
from zope.interface import Interface, Attribute, implementer
from twisted.python.components import registerAdapter
import jinja2 import jinja2
import bcrypt import bcrypt
@ -11,6 +14,18 @@ from core.config import CoreConfig
from core.data import Data from core.data import Data
from core.utils import Utils from core.utils import Utils
class IUserSession(Interface):
userId = Attribute("User's ID")
current_ip = Attribute("User's current ip address")
permissions = Attribute("User's permission level")
@implementer(IUserSession)
class UserSession(object):
def __init__(self, session):
self.userId = 0
self.current_ip = "0.0.0.0"
self.permissions = 0
class FrontendServlet(resource.Resource): class FrontendServlet(resource.Resource):
def getChild(self, name: bytes, request: Request): def getChild(self, name: bytes, request: Request):
self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") self.logger.debug(f"{request.getClientIP()} -> {name.decode()}")
@ -38,6 +53,7 @@ class FrontendServlet(resource.Resource):
self.logger.setLevel(cfg.frontend.loglevel) self.logger.setLevel(cfg.frontend.loglevel)
coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str) coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str)
registerAdapter(UserSession, Session, IUserSession)
fe_game = FE_Game(cfg, self.environment) fe_game = FE_Game(cfg, self.environment)
games = Utils.get_all_titles() games = Utils.get_all_titles()
@ -60,7 +76,7 @@ class FrontendServlet(resource.Resource):
def render_GET(self, request): def render_GET(self, request):
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
template = self.environment.get_template("core/frontend/index.jinja") template = self.environment.get_template("core/frontend/index.jinja")
return template.render(server_name=self.config.server.name, title=self.config.server.name, game_list=self.game_list).encode("utf-16") return template.render(server_name=self.config.server.name, title=self.config.server.name, game_list=self.game_list, sesh=vars(IUserSession(request.getSession()))).encode("utf-16")
class FE_Base(resource.Resource): class FE_Base(resource.Resource):
""" """
@ -80,6 +96,12 @@ class FE_Gate(FE_Base):
def render_GET(self, request: Request): def render_GET(self, request: Request):
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
uri: str = request.uri.decode() uri: str = request.uri.decode()
sesh = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId > 0:
return redirectTo(b"/user", request)
if uri.startswith("/gate/create"): if uri.startswith("/gate/create"):
return self.create_user(request) return self.create_user(request)
@ -92,7 +114,7 @@ class FE_Gate(FE_Base):
else: err = 0 else: err = 0
template = self.environment.get_template("core/frontend/gate/gate.jinja") template = self.environment.get_template("core/frontend/gate/gate.jinja")
return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err).encode("utf-16") return template.render(title=f"{self.core_config.server.name} | Login Gate", error=err, sesh=vars(usr_sesh)).encode("utf-16")
def render_POST(self, request: Request): def render_POST(self, request: Request):
uri = request.uri.decode() uri = request.uri.decode()
@ -100,7 +122,7 @@ class FE_Gate(FE_Base):
if uri == "/gate/gate.login": if uri == "/gate/gate.login":
access_code: str = request.args[b"access_code"][0].decode() access_code: str = request.args[b"access_code"][0].decode()
passwd: str = request.args[b"passwd"][0] passwd: bytes = request.args[b"passwd"][0]
if passwd == b"": if passwd == b"":
passwd = None passwd = None
@ -109,20 +131,22 @@ class FE_Gate(FE_Base):
return redirectTo(b"/gate?e=1", request) return redirectTo(b"/gate?e=1", request)
if passwd is None: if passwd is None:
sesh = self.data.user.login(uid, ip=ip) sesh = self.data.user.check_password(uid)
if sesh is not None: if sesh is not None:
return redirectTo(f"/gate/create?ac={access_code}".encode(), request) return redirectTo(f"/gate/create?ac={access_code}".encode(), request)
return redirectTo(b"/gate?e=1", request) return redirectTo(b"/gate?e=1", request)
salt = bcrypt.gensalt() if not self.data.user.check_password(uid, passwd):
hashed = bcrypt.hashpw(passwd, salt)
sesh = self.data.user.login(uid, hashed, ip)
if sesh is None:
return redirectTo(b"/gate?e=1", request) return redirectTo(b"/gate?e=1", request)
request.addCookie('session', sesh) self.logger.info(f"Successful login of user {uid} at {ip}")
sesh = request.getSession()
usr_sesh = IUserSession(sesh)
usr_sesh.userId = uid
usr_sesh.current_ip = ip
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
elif uri == "/gate/gate.create": elif uri == "/gate/gate.create":
@ -142,10 +166,8 @@ class FE_Gate(FE_Base):
if result is None: if result is None:
return redirectTo(b"/gate?e=3", request) return redirectTo(b"/gate?e=3", request)
sesh = self.data.user.login(uid, hashed, ip) if not self.data.user.check_password(uid, passwd.encode()):
if sesh is None:
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
request.addCookie('session', sesh)
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
@ -159,15 +181,19 @@ class FE_Gate(FE_Base):
ac = request.args[b'ac'][0].decode() ac = request.args[b'ac'][0].decode()
template = self.environment.get_template("core/frontend/gate/create.jinja") template = self.environment.get_template("core/frontend/gate/create.jinja")
return template.render(title=f"{self.core_config.server.name} | Create User", code=ac).encode("utf-16") return template.render(title=f"{self.core_config.server.name} | Create User", code=ac, sesh={"userId": 0}).encode("utf-16")
class FE_User(FE_Base): class FE_User(FE_Base):
def render_GET(self, request: Request): def render_GET(self, request: Request):
template = self.environment.get_template("core/frontend/user/index.jinja") template = self.environment.get_template("core/frontend/user/index.jinja")
return template.render().encode("utf-16")
if b'session' not in request.cookies: sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
if usr_sesh.userId == 0:
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
return template.render(title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)).encode("utf-16")
class FE_Game(FE_Base): class FE_Game(FE_Base):
isLeaf = False isLeaf = False
children: Dict[str, Any] = {} children: Dict[str, Any] = {}

View File

@ -2,10 +2,23 @@
{% block content %} {% block content %}
<h1>Gate</h1> <h1>Gate</h1>
{% include "core/frontend/widgets/err_banner.jinja" %} {% include "core/frontend/widgets/err_banner.jinja" %}
<style>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post"> <form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
<div class="form-group row"> <div class="form-group row">
<label for="access_code">Card Access Code</label><br> <label for="access_code">Card Access Code</label><br>
<input form="login" class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" maxlength="20" required> <input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="passwd">Password</label><br> <label for="passwd">Password</label><br>

View File

@ -9,5 +9,10 @@
</div> </div>
</div> </div>
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;"> <div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
{% if sesh is defined and sesh["userId"] > 0 %}
<a href="/user"><button class="btn btn-primary">Account</button></a>
{% else %}
<a href="/gate"><button class="btn btn-primary">Gate</button></a> <a href="/gate"><button class="btn btn-primary">Gate</button></a>
{% endif %}
</div> </div>