Skip to content

Commit b217bf4

Browse files
committed
Dev : The new Updates and features
1 parent e692f82 commit b217bf4

49 files changed

Lines changed: 3061 additions & 779 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 415 additions & 197 deletions
Large diffs are not rendered by default.

images/jsweb_logo.png

39.3 KB
Loading

jsweb/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from jsweb.app import *
22
from jsweb.server import *
3-
from jsweb.template import *
43
from jsweb.response import *
4+
from jsweb.auth import login_required, login_user, logout_user, get_current_user
5+
from jsweb.security import generate_password_hash, check_password_hash
6+
from jsweb.forms import *
7+
from jsweb.validators import *
8+
from jsweb.blueprints import Blueprint
59

6-
__VERSION__ = "0.1.0"
10+
# Make url_for easily accessible
11+
from .response import url_for
12+
13+
__VERSION__ = "0.2.0"

jsweb/app.py

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,107 @@
1-
from jsweb.database import init_db
2-
from jsweb.routing import Router
3-
from jsweb.request import Request
4-
from jsweb.static import serve_static
5-
# Assuming you have a response module as suggested
6-
from jsweb.response import Response, HTMLResponse
7-
1+
import secrets
2+
import os
3+
from .routing import Router
4+
from .request import Request
5+
from .response import Response, HTMLResponse, configure_template_env
6+
from .auth import init_auth, get_current_user
7+
from .middleware import StaticFilesMiddleware, DBSessionMiddleware, CSRFMiddleware
8+
from .blueprints import Blueprint
89

910
class JsWebApp:
1011
"""
1112
The main application class for the JsWeb framework.
12-
13-
It is responsible for routing requests and is configurable.
14-
Database schema management should be handled by a separate CLI command.
1513
"""
16-
def __init__(self, static_url="/static", static_dir="static", template_dir="templates", db_url=None):
14+
def __init__(self, config):
1715
self.router = Router()
1816
self.template_filters = {}
19-
if db_url:
20-
init_db(db_url)
21-
# Make static and template paths configurable
22-
self.static_url = static_url
23-
self.static_dir = static_dir
24-
self.template_dir = template_dir
25-
26-
def route(self, path, methods=None):
27-
"""A decorator to register a view function for a given URL path."""
28-
if methods is None:
29-
methods = ["GET"]
30-
return self.router.route(path, methods)
17+
self.config = config
18+
self._init_from_config() # Initial setup
3119

32-
def filter(self, name):
33-
"""
34-
A decorator to register a custom filter for use in templates.
35-
The filter is registered with this specific app instance.
36-
"""
20+
def _init_from_config(self):
21+
"""Initializes components that depend on the config."""
22+
if hasattr(self.config, "TEMPLATE_FOLDER") and hasattr(self.config, "BASE_DIR"):
23+
template_path = os.path.join(self.config.BASE_DIR, self.config.TEMPLATE_FOLDER)
24+
configure_template_env(template_path)
25+
26+
if hasattr(self.config, "SECRET_KEY"):
27+
init_auth(self.config.SECRET_KEY, self._get_actual_user_loader())
28+
29+
def _get_actual_user_loader(self):
30+
if hasattr(self, '_user_loader_callback') and self._user_loader_callback:
31+
return self._user_loader_callback
32+
return self.user_loader
3733

34+
def user_loader(self, user_id: int):
35+
try:
36+
from models import User
37+
return User.query.get(user_id)
38+
except (ImportError, AttributeError):
39+
return None
40+
41+
def route(self, path, methods=None, endpoint=None):
42+
return self.router.route(path, methods, endpoint)
43+
44+
def register_blueprint(self, blueprint: Blueprint):
45+
"""Registers a blueprint with the application."""
46+
for path, handler, methods, endpoint in blueprint.routes:
47+
full_path = path
48+
if blueprint.url_prefix:
49+
full_path = f"{blueprint.url_prefix.rstrip('/')}/{path.lstrip('/')}"
50+
51+
# Create a prefixed endpoint, e.g., "auth.login"
52+
full_endpoint = f"{blueprint.name}.{endpoint}"
53+
self.router.add_route(full_path, handler, methods, endpoint=full_endpoint)
54+
55+
def filter(self, name):
3856
def decorator(func):
3957
self.template_filters[name] = func
4058
return func
41-
4259
return decorator
4360

44-
def __call__(self, environ, start_response):
45-
"""The main WSGI entry point."""
46-
req = Request(environ)
47-
48-
# Handle static files using the configured path
49-
if req.path.startswith(self.static_url):
50-
content, status, headers = serve_static(req.path, self.static_url, self.static_dir)
51-
start_response(status, headers)
52-
# Ensure content is bytes
53-
return [content if isinstance(content, bytes) else content.encode("utf-8")]
61+
def _wsgi_app_handler(self, environ, start_response):
62+
req = environ['jsweb.request']
5463

55-
# Resolve and handle dynamic routes
56-
handler = self.router.resolve(req.path, req.method)
64+
handler, params = self.router.resolve(req.path, req.method)
5765
if handler:
58-
response = handler(req)
66+
response = handler(req, **params)
5967

60-
# If a handler returns a raw string, wrap it in a default response object
6168
if isinstance(response, str):
6269
response = HTMLResponse(response)
6370

64-
# If it's not a Response object, it's an error
6571
if not isinstance(response, Response):
6672
raise TypeError(f"View function did not return a Response object (got {type(response).__name__})")
6773

68-
# Convert our Response object to what the WSGI server needs
74+
if hasattr(req, 'new_csrf_token_generated') and req.new_csrf_token_generated:
75+
response.set_cookie("csrf_token", req.csrf_token, httponly=False, samesite='Lax')
76+
6977
body_bytes, status, headers = response.to_wsgi()
7078
start_response(status, headers)
7179
return [body_bytes]
7280

73-
# Handle 404 Not Found
7481
start_response("404 Not Found", [("Content-Type", "text/html")])
75-
return [b"<h1>404 Not Found</h1>"]
82+
return [b"<h1>404 Not Found</h1>"]
83+
84+
def __call__(self, environ, start_response):
85+
# Create the Request object ONCE and pass the app instance to it.
86+
req = Request(environ, self)
87+
environ['jsweb.request'] = req
88+
89+
csrf_token = req.cookies.get("csrf_token")
90+
req.new_csrf_token_generated = False
91+
if not csrf_token:
92+
csrf_token = secrets.token_hex(32)
93+
req.new_csrf_token_generated = True
94+
req.csrf_token = csrf_token
95+
96+
if hasattr(self.config, "SECRET_KEY"):
97+
req.user = get_current_user(req)
98+
99+
static_url = getattr(self.config, "STATIC_URL", "/static")
100+
static_dir = getattr(self.config, "STATIC_DIR", "static")
101+
102+
handler = self._wsgi_app_handler
103+
handler = DBSessionMiddleware(handler)
104+
handler = StaticFilesMiddleware(handler, static_url, static_dir)
105+
handler = CSRFMiddleware(handler)
106+
107+
return handler(environ, start_response)

jsweb/auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from functools import wraps
2+
from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadTimeSignature
3+
from .response import redirect, url_for
4+
5+
# This will be initialized by the JsWebApp instance
6+
_serializer = None
7+
_user_loader = None
8+
9+
def init_auth(secret_key, user_loader_func):
10+
"""Initializes the authentication system."""
11+
global _serializer, _user_loader
12+
_serializer = URLSafeTimedSerializer(secret_key)
13+
_user_loader = user_loader_func
14+
15+
def login_user(response, user):
16+
"""Logs a user in by creating a secure session cookie."""
17+
session_token = _serializer.dumps(user.id)
18+
response.set_cookie("session", session_token, httponly=True)
19+
20+
def logout_user(response):
21+
"""Logs a user out by deleting the session cookie."""
22+
response.delete_cookie("session")
23+
24+
def get_current_user(request):
25+
"""Gets the currently logged-in user from the session cookie."""
26+
session_token = request.cookies.get("session")
27+
if not session_token:
28+
return None
29+
30+
try:
31+
# The max_age check (e.g., 30 days) is handled by the serializer
32+
user_id = _serializer.loads(session_token, max_age=2592000)
33+
return _user_loader(user_id)
34+
except (SignatureExpired, BadTimeSignature):
35+
return None
36+
37+
def login_required(handler):
38+
"""
39+
A decorator to protect routes from unauthenticated access.
40+
If the user is not logged in, it redirects to the URL for the 'auth.login' endpoint.
41+
"""
42+
@wraps(handler)
43+
def decorated_function(request, *args, **kwargs):
44+
if not request.user:
45+
# Use url_for to dynamically find the login page
46+
login_url = url_for(request, 'auth.login')
47+
return redirect(login_url)
48+
return handler(request, *args, **kwargs)
49+
return decorated_function

jsweb/blueprints.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class Blueprint:
2+
"""
3+
A self-contained, reusable component of a JsWeb application.
4+
Blueprints have their own routes which are later registered with the main app.
5+
"""
6+
def __init__(self, name, url_prefix=None):
7+
"""
8+
Initializes a new Blueprint.
9+
10+
Args:
11+
name (str): The name of the blueprint.
12+
url_prefix (str, optional): A prefix to be added to all routes in this blueprint.
13+
"""
14+
self.name = name
15+
self.url_prefix = url_prefix
16+
self.routes = []
17+
18+
def route(self, path, methods=None, endpoint=None):
19+
"""
20+
A decorator to register a view function for a given path within the blueprint.
21+
"""
22+
if methods is None:
23+
methods = ["GET"]
24+
25+
def decorator(handler):
26+
# If no endpoint is provided, use the function name as the default.
27+
route_endpoint = endpoint or handler.__name__
28+
self.routes.append((path, handler, methods, route_endpoint))
29+
return handler
30+
return decorator

0 commit comments

Comments
 (0)