diff --git a/core/data/database.py b/core/data/database.py index 65b01aa..4ba3b3a 100644 --- a/core/data/database.py +++ b/core/data/database.py @@ -31,7 +31,7 @@ class Data: self.arcade = ArcadeData(self.config, self.session) self.card = CardData(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 = logging.Formatter(log_fmt_str) diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 9e79891..b7f72e2 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -9,6 +9,7 @@ from sqlalchemy.sql import func, select, Delete from uuid import uuid4 from datetime import datetime, timedelta from sqlalchemy.engine import Row +import bcrypt from core.data.schema.base import BaseData, metadata @@ -26,17 +27,6 @@ aime_user = Table( 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): PermUser = 1 PermMod = 2 @@ -74,50 +64,20 @@ class UserData(BaseData): if result is None: return None return result.lastrowid - def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]: - sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd)) - - 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) - + def get_user(self, user_id: int) -> Optional[Row]: + sql = select(aime_user).where(aime_user.c.id == user_id) result = self.execute(sql) if result is None: return False - return True + return result.fetchone() + + def check_password(self, user_id: int, passwd: bytes = None) -> bool: + usr = self.get_user(user_id) + if usr is None: return False - def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]: - cookie = uuid4().hex + if usr['password'] is None: + return False - sql = insert(frontend_session).values( - user = user_id, - ip = ip, - session_cookie = cookie, - expires = expires - ) - - result = self.execute(sql) - if result is None: - return None - return cookie + return bcrypt.checkpw(passwd, usr['password'].encode()) def reset_autoincrement(self, ai_value: int) -> None: # ALTER TABLE isn't in sqlalchemy so we do this the ugly way diff --git a/core/data/schema/versions/CORE_3_rollback.sql b/core/data/schema/versions/CORE_3_rollback.sql new file mode 100644 index 0000000..9132cc3 --- /dev/null +++ b/core/data/schema/versions/CORE_3_rollback.sql @@ -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; \ No newline at end of file diff --git a/core/data/schema/versions/CORE_4_upgrade.sql b/core/data/schema/versions/CORE_4_upgrade.sql new file mode 100644 index 0000000..6a04c74 --- /dev/null +++ b/core/data/schema/versions/CORE_4_upgrade.sql @@ -0,0 +1 @@ +DROP TABLE `frontend_session`; \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py index 780698e..6f95073 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -4,6 +4,9 @@ from twisted.web import resource from twisted.web.util import redirectTo from twisted.web.http import Request 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 bcrypt @@ -11,6 +14,18 @@ from core.config import CoreConfig from core.data import Data 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): def getChild(self, name: bytes, request: Request): self.logger.debug(f"{request.getClientIP()} -> {name.decode()}") @@ -38,6 +53,7 @@ class FrontendServlet(resource.Resource): self.logger.setLevel(cfg.frontend.loglevel) coloredlogs.install(level=cfg.frontend.loglevel, logger=self.logger, fmt=log_fmt_str) + registerAdapter(UserSession, Session, IUserSession) fe_game = FE_Game(cfg, self.environment) games = Utils.get_all_titles() @@ -59,8 +75,8 @@ class FrontendServlet(resource.Resource): def render_GET(self, request): self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}") - 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") + 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, sesh=vars(IUserSession(request.getSession()))).encode("utf-16") class FE_Base(resource.Resource): """ @@ -80,6 +96,12 @@ class FE_Gate(FE_Base): def render_GET(self, request: Request): self.logger.debug(f"{request.getClientIP()} -> {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"): return self.create_user(request) @@ -92,7 +114,7 @@ class FE_Gate(FE_Base): else: err = 0 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): uri = request.uri.decode() @@ -100,7 +122,7 @@ class FE_Gate(FE_Base): if uri == "/gate/gate.login": 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"": passwd = None @@ -109,20 +131,22 @@ class FE_Gate(FE_Base): return redirectTo(b"/gate?e=1", request) if passwd is None: - sesh = self.data.user.login(uid, ip=ip) + sesh = self.data.user.check_password(uid) if sesh is not None: return redirectTo(f"/gate/create?ac={access_code}".encode(), request) return redirectTo(b"/gate?e=1", request) - salt = bcrypt.gensalt() - hashed = bcrypt.hashpw(passwd, salt) - sesh = self.data.user.login(uid, hashed, ip) - - if sesh is None: + if not self.data.user.check_password(uid, passwd): 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) elif uri == "/gate/gate.create": @@ -142,10 +166,8 @@ class FE_Gate(FE_Base): if result is None: return redirectTo(b"/gate?e=3", request) - sesh = self.data.user.login(uid, hashed, ip) - if sesh is None: + if not self.data.user.check_password(uid, passwd.encode()): return redirectTo(b"/gate", request) - request.addCookie('session', sesh) return redirectTo(b"/user", request) @@ -159,14 +181,18 @@ class FE_Gate(FE_Base): ac = request.args[b'ac'][0].decode() 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): def render_GET(self, request: Request): 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 template.render(title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh)).encode("utf-16") class FE_Game(FE_Base): isLeaf = False diff --git a/core/frontend/gate/gate.jinja b/core/frontend/gate/gate.jinja index 760fbab..4f4e2fb 100644 --- a/core/frontend/gate/gate.jinja +++ b/core/frontend/gate/gate.jinja @@ -2,10 +2,23 @@ {% block content %}

Gate

{% include "core/frontend/widgets/err_banner.jinja" %} +

- +

diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja index 6bef3e3..d196361 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/frontend/widgets/topbar.jinja @@ -9,5 +9,10 @@
+ {% if sesh is defined and sesh["userId"] > 0 %} + + {% else %} + {% endif %} +
\ No newline at end of file