diff --git a/src/judas_server/__main__.py b/src/judas_server/__main__.py index c8b8fdb..ce09edf 100644 --- a/src/judas_server/__main__.py +++ b/src/judas_server/__main__.py @@ -3,7 +3,7 @@ import logging as lg if __name__ == "__main__": - from judas_server.web.server import JudasWebServer + from judas_server.web.web_server import JudasWebServer lg.basicConfig( level=lg.DEBUG, diff --git a/src/judas_server/web/routes/__init__.py b/src/judas_server/web/routes/__init__.py index e69de29..2ac5931 100644 --- a/src/judas_server/web/routes/__init__.py +++ b/src/judas_server/web/routes/__init__.py @@ -0,0 +1,5 @@ +from .auth import auth_bp +from .index import index_bp +from .panel import panel_bp + +__all__ = ["auth_bp", "index_bp", "panel_bp"] diff --git a/src/judas_server/web/routes/auth.py b/src/judas_server/web/routes/auth.py new file mode 100644 index 0000000..c9043f7 --- /dev/null +++ b/src/judas_server/web/routes/auth.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +from typing import TYPE_CHECKING + +import flask +import flask_login + +from judas_server.web.user import User + +if TYPE_CHECKING: + from werkzeug.wrappers import Response + +auth_bp: flask.Blueprint = flask.Blueprint( + "auth", __name__, url_prefix="/auth" +) + + +@auth_bp.route("/login", methods=["GET", "POST"]) +def login() -> Response | str: + """Handles user login via password form.""" + if flask.request.method == "POST": + password = flask.request.form.get("password", "") + if password == flask.current_app.config["PASSWORD"]: + user = User("admin") + flask_login.login_user(user) + next_page = flask.request.args.get("next") + return flask.redirect(next_page or flask.url_for("index.index")) + # return flask.redirect(flask.url_for("panel.panel")) + else: + return flask.render_template( + "login.html", + error="Invalid password. Please try again.", + ) + return flask.render_template("login.html") + + +@auth_bp.route("/logout") +@flask_login.login_required +def logout() -> Response: + """Logs out the current user.""" + flask_login.logout_user() + return flask.redirect(flask.url_for("index.index")) diff --git a/src/judas_server/web/routes/index.py b/src/judas_server/web/routes/index.py new file mode 100644 index 0000000..7f0c0b6 --- /dev/null +++ b/src/judas_server/web/routes/index.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +from typing import TYPE_CHECKING + +import flask +import flask_login + + +if TYPE_CHECKING: + from werkzeug.wrappers import Response + +index_bp: flask.Blueprint = flask.Blueprint("index", __name__) + + +@index_bp.route("/") +def index() -> Response | str: + """Renders the index page.""" + return flask.render_template( + "index.html", logged=flask_login.current_user.is_authenticated + ) diff --git a/src/judas_server/web/routes/panel.py b/src/judas_server/web/routes/panel.py new file mode 100644 index 0000000..73c3962 --- /dev/null +++ b/src/judas_server/web/routes/panel.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +from typing import TYPE_CHECKING + +import flask +import flask_login + + +if TYPE_CHECKING: + from werkzeug.wrappers import Response + +panel_bp: flask.Blueprint = flask.Blueprint( + "panel", __name__, url_prefix="/panel" +) + + +@panel_bp.route("/") +@flask_login.login_required +def panel() -> str: + """Renders the main panel page with PC details. + + Returns: + Rendered HTML template with PC details. + """ + return flask.render_template( + "panel.html", username=flask_login.current_user.id, pcs={} + ) diff --git a/src/judas_server/web/static/css/palette.css b/src/judas_server/web/static/css/palette.css new file mode 100644 index 0000000..db0a307 --- /dev/null +++ b/src/judas_server/web/static/css/palette.css @@ -0,0 +1,80 @@ +:root { + --ctp-rosewater: #f5e0dc; + --ctp-rosewater-rgb: 245 224 220; + --ctp-rosewater-hsl: 9.600 55.556% 91.176%; + --ctp-flamingo: #f2cdcd; + --ctp-flamingo-rgb: 242 205 205; + --ctp-flamingo-hsl: 0.000 58.730% 87.647%; + --ctp-pink: #f5c2e7; + --ctp-pink-rgb: 245 194 231; + --ctp-pink-hsl: 316.471 71.831% 86.078%; + --ctp-mauve: #cba6f7; + --ctp-mauve-rgb: 203 166 247; + --ctp-mauve-hsl: 267.407 83.505% 80.980%; + --ctp-red: #f38ba8; + --ctp-red-rgb: 243 139 168; + --ctp-red-hsl: 343.269 81.250% 74.902%; + --ctp-maroon: #eba0ac; + --ctp-maroon-rgb: 235 160 172; + --ctp-maroon-hsl: 350.400 65.217% 77.451%; + --ctp-peach: #fab387; + --ctp-peach-rgb: 250 179 135; + --ctp-peach-hsl: 22.957 92.000% 75.490%; + --ctp-yellow: #f9e2af; + --ctp-yellow-rgb: 249 226 175; + --ctp-yellow-hsl: 41.351 86.047% 83.137%; + --ctp-green: #a6e3a1; + --ctp-green-rgb: 166 227 161; + --ctp-green-hsl: 115.455 54.098% 76.078%; + --ctp-teal: #94e2d5; + --ctp-teal-rgb: 148 226 213; + --ctp-teal-hsl: 170.000 57.353% 73.333%; + --ctp-sky: #89dceb; + --ctp-sky-rgb: 137 220 235; + --ctp-sky-hsl: 189.184 71.014% 72.941%; + --ctp-sapphire: #74c7ec; + --ctp-sapphire-rgb: 116 199 236; + --ctp-sapphire-hsl: 198.500 75.949% 69.020%; + --ctp-blue: #89b4fa; + --ctp-blue-rgb: 137 180 250; + --ctp-blue-hsl: 217.168 91.870% 75.882%; + --ctp-lavender: #b4befe; + --ctp-lavender-rgb: 180 190 254; + --ctp-lavender-hsl: 231.892 97.368% 85.098%; + --ctp-text: #cdd6f4; + --ctp-text-rgb: 205 214 244; + --ctp-text-hsl: 226.154 63.934% 88.039%; + --ctp-subtext1: #bac2de; + --ctp-subtext1-rgb: 186 194 222; + --ctp-subtext1-hsl: 226.667 35.294% 80.000%; + --ctp-subtext0: #a6adc8; + --ctp-subtext0-rgb: 166 173 200; + --ctp-subtext0-hsl: 227.647 23.611% 71.765%; + --ctp-overlay2: #9399b2; + --ctp-overlay2-rgb: 147 153 178; + --ctp-overlay2-hsl: 228.387 16.757% 63.725%; + --ctp-overlay1: #7f849c; + --ctp-overlay1-rgb: 127 132 156; + --ctp-overlay1-hsl: 229.655 12.775% 55.490%; + --ctp-overlay0: #6c7086; + --ctp-overlay0-rgb: 108 112 134; + --ctp-overlay0-hsl: 230.769 10.744% 47.451%; + --ctp-surface2: #585b70; + --ctp-surface2-rgb: 88 91 112; + --ctp-surface2-hsl: 232.500 12.000% 39.216%; + --ctp-surface1: #45475a; + --ctp-surface1-rgb: 69 71 90; + --ctp-surface1-hsl: 234.286 13.208% 31.176%; + --ctp-surface0: #313244; + --ctp-surface0-rgb: 49 50 68; + --ctp-surface0-hsl: 236.842 16.239% 22.941%; + --ctp-base: #1e1e2e; + --ctp-base-rgb: 30 30 46; + --ctp-base-hsl: 240.000 21.053% 14.902%; + --ctp-mantle: #181825; + --ctp-mantle-rgb: 24 24 37; + --ctp-mantle-hsl: 240.000 21.311% 11.961%; + --ctp-crust: #11111b; + --ctp-crust-rgb: 17 17 27; + --ctp-crust-hsl: 240.000 22.727% 8.627%; +} diff --git a/src/judas_server/web/static/css/style.css b/src/judas_server/web/static/css/style.css index 8ca23c8..c0a9a23 100644 --- a/src/judas_server/web/static/css/style.css +++ b/src/judas_server/web/static/css/style.css @@ -1,22 +1,4 @@ -:root { - --nord-bg0: #2e3440; - --nord-bg1: #3b4252; - --nord-bg2: #434c5e; - --nord-bg3: #4c566a; - --nord-fg0: #eceff4; - --nord-fg1: #e5e9f0; - --nord-fg2: #d8dee9; - --nord-acc0: #8fbcbb; - --nord-acc1: #88c0d0; - --nord-acc2: #81a1c1; - --nord-acc3: #5e81ac; - --nord-aur0: #bf616a; - --nord-aur1: #d08770; - --nord-aur2: #ebcb8b; - --nord-aur3: #a3be8c; - --nord-aur4: #b48ead; - -} +@import "palette.css"; * { margin: 0; @@ -25,22 +7,22 @@ } body { - background-color: var(--nord-aur4); + background-color: var(--ctp-base); font-family: monospace, sans-serif !important; - color: var(--nord-fg0); + color: var(--ctp-text); } input { font-family: inherit; color: #eceff4; - background-color: var(--nord-bg2); + background-color: var(--ctp-crust); border: none; border-radius: 0.25rem; padding: 0.25rem; } a { - color: var(--nord-acc0); + color: var(--ctp-blue); text-decoration: none; } @@ -54,8 +36,8 @@ header { display: flex; justify-content: space-between; align-items: center; - background-color: var(--nord-bg0); - color: var(--nord-fg0); + background-color: var(--ctp-mantle); + /* color: var(--ctp-text); */ padding: 1rem; } @@ -64,34 +46,34 @@ main { flex-direction: column; gap: 1rem; padding: 1rem; - background-color: var(--nord-bg1); + /* background-color: var(--ctp-base); */ flex-grow: 1; text-align: center; } header a { text-decoration: none; - color: var(--nord-fg0); + /* color: var(--nord-fg0); */ } .button { display: inline-block; padding: 0.5rem 1rem; - background-color: var(--nord-acc0); - color: var(--nord-bg0); + background-color: var(--ctp-lavender); + color: var(--ctp-base); border: none; border-radius: 0.25rem; text-decoration: none; transition: 0.3s ease-in-out; } -.button a:hover { - color: var(--nord-bg0); - text-decoration: none; -} +/* .button a:hover { */ +/* color: var(--nord-bg0); */ +/* text-decoration: none; */ +/* } */ .button:hover { - background-color: var(--nord-acc2); + background-color: var(--ctp-mauve); cursor: pointer; } @@ -106,9 +88,9 @@ header a { justify-content: center; margin: 1rem; padding: 1rem; - background-color: var(--nord-aur0); - color: var(--nord-fg0); - border: 6px solid var(--nord-aur1); + background-color: var(--ctp-surface0); + color: var(--ctp-red); + border: 6px solid var(--ctp-red); border-radius: 24px; } @@ -117,9 +99,9 @@ header a { width: 100%; } -.select-table{ +.select-table { border-collapse: collapse; - border: 2px solid var(--nord-fg0); + border: 2px solid var(--ctp-surface0); } .select-table thead { @@ -131,18 +113,18 @@ header a { .select-table th, .select-table td { padding: 0.5rem; text-align: center; - border: 1px solid var(--nord-fg1); + border: 1px solid var(--ctp-surface0); border-collapse: collapse; } .select-table th { - background-color: var(--nord-bg2); + background-color: var(--ctp-surface1); color: var(--nord-fg0); } .select-table a { display: block; - color: var(--nord-acc0); + /* color: var(--nord-acc0); */ text-decoration: none; transition: 0.1s ease-in-out; } @@ -152,23 +134,23 @@ header a { } .select-table tr:hover { - background-color: var(--nord-acc1); + background-color: var(--ctp-surface0); } .select-table tr:hover a { - color: var(--nord-bg0); + color: var(--ctp-base); } .red-bg { - background-color: var(--nord-aur0); + background-color: var(--ctp-red); } .yellow-bg { - background-color: var(--nord-aur2); + background-color: var(--ctp-yellow); } .green-bg { - background-color: var(--nord-aur3); + background-color: var(--ctp-green); } .button-container { @@ -183,24 +165,24 @@ header a { } .link:hover { - color: var(--nord-acc2); + color: var(--ctp-blue); } .details-table tr td:first-child { font-weight: 900; - background-color: var(--nord-bg3); + background-color: var(--ctp-surface0); white-space: nowrap; width: 1%; } .details-table { border-collapse: collapse; - border: 2px solid var(--nord-fg0); + border: 2px solid var(--ctp-surface1); } .details-table th, .details-table td { padding: 0.5rem; text-align: left; - border: 1px solid var(--nord-fg1); + border: 1px solid var(--ctp-text); border-collapse: collapse; -} \ No newline at end of file +} diff --git a/src/judas_server/web/templates/details.html b/src/judas_server/web/templates/details.html index 81c4f6c..778af20 100644 --- a/src/judas_server/web/templates/details.html +++ b/src/judas_server/web/templates/details.html @@ -9,7 +9,7 @@
-

judas

+

judas

Logout

diff --git a/src/judas_server/web/templates/index.html b/src/judas_server/web/templates/index.html index 01cb969..457890e 100644 --- a/src/judas_server/web/templates/index.html +++ b/src/judas_server/web/templates/index.html @@ -9,11 +9,11 @@
-

judas

+

judas

{% if logged %} -

Logout

+

Logout

{% else %} -

Login

+

Login

{% endif %}
@@ -24,9 +24,9 @@

Notice: Please use this system responsibly and in accordance with all applicable laws and organizational policies.

{% if logged %} -

Go to panel

+

Go to panel

{% else %} -

Please log in to manage your remote PCs.

+

Please log in to manage your remote PCs.

{% endif %}
diff --git a/src/judas_server/web/templates/login.html b/src/judas_server/web/templates/login.html index c141186..f1db819 100644 --- a/src/judas_server/web/templates/login.html +++ b/src/judas_server/web/templates/login.html @@ -9,7 +9,7 @@
-

judas

+

judas

Login

diff --git a/src/judas_server/web/templates/panel.html b/src/judas_server/web/templates/panel.html index d15fe6b..2e74380 100644 --- a/src/judas_server/web/templates/panel.html +++ b/src/judas_server/web/templates/panel.html @@ -9,8 +9,8 @@
-

judas

-

Logout

+

judas

+

Logout

Select a PC to Control

@@ -39,4 +39,4 @@
- \ No newline at end of file + diff --git a/src/judas_server/web/templates/stream.html b/src/judas_server/web/templates/stream.html deleted file mode 100644 index 7f53b35..0000000 --- a/src/judas_server/web/templates/stream.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - judas panel - stream - - -
-
-

judas

-

Logout

-
-
- - \ No newline at end of file diff --git a/src/judas_server/web/user.py b/src/judas_server/web/user.py new file mode 100644 index 0000000..d26e40e --- /dev/null +++ b/src/judas_server/web/user.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import flask_login + + +class User(flask_login.UserMixin): + """Represents a user for authentication purposes.""" + + def __init__(self, id: str) -> None: + super().__init__() + self.id = id + + def get_id(self) -> str: + """Return the unique identifier for the user.""" + return self.id + + def __str__(self) -> str: + return f"User(id={self.id})" + + def __repr__(self) -> str: + return f"User(id={self.id})" + + +def load_user(user_id): + if user_id == "admin": + return User("admin") + return None diff --git a/src/judas_server/web/web_server.py b/src/judas_server/web/web_server.py index afc5168..1947f46 100644 --- a/src/judas_server/web/web_server.py +++ b/src/judas_server/web/web_server.py @@ -1,171 +1,53 @@ # -*- coding: utf-8 -*- -from typing import Final +from __future__ import annotations -import flask -import flask_login +import logging as lg +from flask import Flask +from flask_login import LoginManager -PASSWORD: Final[str] = "123" - -app = flask.Flask(__name__) -app.secret_key = "dildo" - -login_manager = flask_login.LoginManager() -login_manager.init_app(app) +from judas_server.web.user import load_user -PC_DETAILS = { - "PC1": { - "id": "PC1", - "status": "online", - "last_seen": "2023-10-01 12:00:00", - }, - "PC2": { - "id": "PC2", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, - "PC3": { - "id": "PC3", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, - "PC4": { - "id": "PC4", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, - "PC5": { - "id": "PC5", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, - "PC6": { - "id": "PC6", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, - "PC7": { - "id": "PC7", - "status": "offline", - "last_seen": "2023-10-01 11:00:00", - }, -} +class JudasWebServer: + def __init__(self, secret_key: str) -> None: + self.logger: lg.Logger = lg.getLogger( + f"{__name__}.{self.__class__.__name__}" + ) + self.logger.debug("Initializing JudasWebServer...") + self.app: Flask = Flask( + __name__, static_folder="static", template_folder="templates" + ) + self.app.secret_key = secret_key -class User(flask_login.UserMixin): - """Represents a user for authentication purposes.""" + # hard-code password + self.app.config["PASSWORD"] = "123" - def __init__(self, id: str): - super().__init__() - self.id = id + # extensions + self.login_manager: LoginManager = LoginManager() + self.app.config["LoginManager"] = self.login_manager + self.configure_extensions() + self.init_routes() -@login_manager.user_loader -def load_user(user_id: str) -> User | None: - """Loads a user by user_id. + def configure_extensions(self) -> None: + self.logger.debug("Configuring extensions...") + self.login_manager.init_app(self.app) + self.login_manager.user_loader(load_user) + self.login_manager.login_view = "auth.login" - Args: - user_id: The ID of the user. + # TODO: add login page - Returns: - The User object if found, else None. - """ - if user_id == "admin": - return User("admin") - return None + def init_routes(self) -> None: + self.logger.debug("Initializing routes...") + from judas_server.web.routes import auth_bp, index_bp, panel_bp + self.app.register_blueprint(index_bp) + self.app.register_blueprint(auth_bp) + self.app.register_blueprint(panel_bp) -@app.route("/") -def index() -> flask.Response | str: - """Renders the index page with a link to the login page.""" - return flask.render_template( - "index.html", logged=flask_login.current_user.is_authenticated - ) - - -@app.route("/login", methods=["GET", "POST"]) -def login() -> flask.Response | str: - """Handles user login via password form.""" - if flask.request.method == "POST": - password = flask.request.form.get("password", "") - if password == PASSWORD: - user = User("admin") - flask_login.login_user(user) - return flask.redirect(flask.url_for("panel")) - else: - return flask.render_template( - "login.html", - error="Invalid password. Please try again.", - ) - return flask.render_template("login.html") - - -@app.route("/logout") -@flask_login.login_required -def logout() -> str: - """Logs out the current user.""" - flask_login.logout_user() - return flask.redirect(flask.url_for("index")) - - -@app.route("/panel") -@flask_login.login_required -def panel() -> str: - """Renders the main panel page with PC details. - - Returns: - Rendered HTML template with PC details. - """ - return flask.render_template( - "panel.html", - username=flask_login.current_user.id, - pcs=PC_DETAILS, - ) - - -@app.route("/details/") -@flask_login.login_required -def details(pc_id: str) -> str: - """Renders the details page for a specific PC. - - Args: - pc_id: The ID of the PC to display details for. - - Returns: - Rendered HTML template with PC details. - """ - return flask.render_template( - "details.html", - username=flask_login.current_user.id, - pc=PC_DETAILS[pc_id], - ) - - -@app.route("/stream/") -@flask_login.login_required -def stream(pc_id: str) -> str: - """Renders the stream page for a specific PC. - - Args: - pc_id: The ID of the PC to stream from. - - Returns: - Rendered HTML template for streaming. - """ - return flask.render_template( - "stream.html", - logged=True, - username=flask_login.current_user.id, - pc=PC_DETAILS[pc_id], - ) - - -@app.route("/stream_feed/") -@flask_login.login_required -def stream_feed(pc_id: str) -> flask.Response: - return flask.Response(mimetype="multipart/x-mixed-replace; boundary=frame") - - -if __name__ == "__main__": - app.run(debug=True) + def run(self, host: str = "0.0.0.0", port: int = 5000) -> None: + self.logger.info(f"Starting web server on {host}:{port}...") + self.app.run(host=host, port=port) + self.logger.info("Server stopped.")