Skip to content

Commit a143a3a

Browse files
refactor(core,auth,admin): improve database operations and authentication flow
- Refactor database query operations and model management - Update authentication dependencies and JWT handling - Improve admin URLs and user management - Add new migrations for example project - Fix createsuperuser command and middleware - Update example project configuration and views
1 parent 3817739 commit a143a3a

File tree

23 files changed

+317
-344
lines changed

23 files changed

+317
-344
lines changed

example/apps/home/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from raystack.shortcuts import render
1+
from raystack.shortcuts import render_template
22

33
# Create your views here.
44

55
def home_view(request):
6-
return render(request, "home/home.html", context={"framework": "Raystack"})
6+
return render_template(request, "home/home.html", context={"framework": "Raystack"})

example/config/settings.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
# Base project directory
44
BASE_DIR = pathlib.Path(__file__).resolve().parent.parent
55

6+
# API settings
7+
API_V1_STR = "/api/v1"
8+
69
# Database settings
710
DATABASES = {
811
'default': {
912
'ENGINE': 'raystack.core.database.sqlalchemy',
10-
# Synchronous mode (default)
13+
# Synchronous mode (for management commands)
1114
'URL': 'sqlite:///' + str(BASE_DIR / 'db.sqlite3'),
1215

13-
# Asynchronous mode (uncomment to use)
16+
# Asynchronous mode (for web server)
1417
# 'URL': 'sqlite+aiosqlite:///' + str(BASE_DIR / 'db.sqlite3'),
1518

1619
# Other examples of asynchronous URLs:
@@ -24,9 +27,11 @@
2427
DEBUG = True
2528

2629
INSTALLED_APPS = [
27-
'raystack.contrib.admin',
28-
'raystack.contrib.auth',
2930
'apps.home',
31+
'raystack.contrib.admin',
32+
'raystack.contrib.auth.users',
33+
'raystack.contrib.auth.accounts',
34+
'raystack.contrib.auth.groups',
3035
]
3136

3237
TEMPLATES = [
@@ -47,8 +52,13 @@
4752

4853
# Static files settings
4954
STATICFILES_DIRS = [
50-
str(BASE_DIR.parent / "src" / "raystack" / "contrib" / "static"),
55+
str(BASE_DIR.parent / "raystack" / "src" / "raystack" / "contrib" / "static"),
5156
str(BASE_DIR / "static"),
5257
]
5358

54-
STATIC_ROOT = str(BASE_DIR / "staticfiles")
59+
STATIC_ROOT = str(BASE_DIR / "staticfiles")
60+
61+
# Middleware settings
62+
MIDDLEWARE = [
63+
'raystack.middlewares.SimpleAuthMiddleware',
64+
]

example/core/__init__.py

Lines changed: 1 addition & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,4 @@
1-
from fastapi import FastAPI, Request
21
from raystack import Raystack
3-
from fastapi.responses import JSONResponse, HTMLResponse
4-
from jose import JWTError, jwt
5-
from config.settings import SECRET_KEY, ALGORITHM
6-
from starlette.middleware.authentication import AuthenticationMiddleware
7-
from starlette.authentication import (
8-
AuthenticationBackend, AuthenticationError, SimpleUser, UnauthenticatedUser,
9-
AuthCredentials
10-
)
11-
from starlette.middleware.sessions import SessionMiddleware
12-
13-
from raystack.shortcuts import render_template
14-
15-
16-
from pydantic_settings import BaseSettings
17-
18-
19-
class Settings(BaseSettings):
20-
database_url: str
21-
secret_key: str
22-
debug: bool
23-
youmoney_client_id: str | None = None # Add these fields if needed
24-
youmoney_redirect_url: str | None = None
25-
youmoney_client_secret: str | None = None
26-
youmoney_access_token: str | None = None
27-
youmoney_wallet_number: str | None = None
28-
29-
class Config:
30-
env_file = ".env" # Specifies path to .env file
31-
extra = 'ignore' # Ignore extra fields
322

333
# Initialize object
34-
settings = Settings(
35-
database_url="postgresql://user:password@localhost/dbname",
36-
secret_key="your-secret-key",
37-
debug=True
38-
)
39-
40-
41-
app = Raystack()
42-
43-
44-
from starlette.responses import JSONResponse, \
45-
PlainTextResponse, \
46-
RedirectResponse, \
47-
StreamingResponse, \
48-
FileResponse, \
49-
HTMLResponse
50-
51-
@app.exception_handler(403)
52-
async def not_found(request, exc):
53-
return RedirectResponse("/auth/accounts/login", status_code=303)
54-
# return render_template(request=request, template_name="401.html", context={"request": request})
55-
56-
# Authentication class
57-
class userAuthentication(AuthenticationBackend):
58-
async def authenticate(self, request):
59-
jwt_cookie = request.cookies.get('jwt')
60-
if jwt_cookie: # cookie exists
61-
try:
62-
payload = jwt.decode(jwt_cookie.encode('utf8'), str(SECRET_KEY), algorithms=[ALGORITHM])
63-
return AuthCredentials(["user_auth"]), SimpleUser(payload['user_id'])
64-
except:
65-
raise AuthenticationError('Invalid auth credentials')
66-
else:
67-
return # unauthenticated
68-
69-
# Middleware for tracking history
70-
@app.middleware("http")
71-
async def update_session_history(request, call_next):
72-
response = await call_next(request)
73-
history = request.session.setdefault('history', [])
74-
history.append(request.url.path)
75-
return response
76-
77-
78-
# Middleware for session management
79-
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
80-
81-
# Middleware for authentication
82-
app.add_middleware(AuthenticationMiddleware, backend=userAuthentication())
83-
84-
85-
from apscheduler.schedulers.background import BackgroundScheduler
86-
from datetime import datetime
87-
88-
# # Function that will be executed periodically
89-
# def periodic_task():
90-
# print(f"Periodic task completed: {datetime.now()}")
91-
92-
# # Initialize scheduler
93-
# scheduler = BackgroundScheduler()
94-
95-
# # Add task to execute every 10 seconds
96-
# scheduler.add_job(periodic_task, 'interval', seconds=10)
97-
98-
# # Start scheduler
99-
# scheduler.start()
4+
app = Raystack()

example/manage.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"""Raystack's command-line utility for administrative tasks."""
33
import os
44
import sys
5+
sys.path.insert(0, os.path.abspath('..'))
6+
sys.path.insert(0, os.path.abspath('../src'))
7+
sys.path.insert(0, os.path.abspath('.'))
58

69

710
def main():

example/migrations/env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from sqlalchemy import engine_from_config
33
from sqlalchemy import pool
44
from alembic import context
5-
from raystack.core.database.sqlalchemy import Base
5+
from raystack.core.database.sqlalchemy import Base, db
66

77
config = context.config
88

99
if config.config_file_name is not None:
1010
fileConfig(config.config_file_name)
1111

12-
target_metadata = Base.metadata
12+
target_metadata = db.metadata
1313

1414
def run_migrations_offline():
1515
"""Run migrations in 'offline' mode."""
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Migration: None
2+
3+
Revision ID: 3f6f1a9e861a
4+
Revises:
5+
Create Date: 2025-08-31 07:22:15
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '3f6f1a9e861a'
14+
down_revision = None
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
"""Apply migration."""
21+
# ### commands auto generated by Raystack ###
22+
op.execute('''CREATE TABLE IF NOT EXISTS groups_groupmodel ("id" TEXT PRIMARY KEY, "name" TEXT UNIQUE NOT NULL, "description" TEXT NOT NULL)''')
23+
op.execute('''CREATE TABLE IF NOT EXISTS users_usermodel ("id" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "age" TEXT NOT NULL, "email" TEXT NOT NULL, "password_hash" TEXT NOT NULL, "group" TEXT NOT NULL, "organization" TEXT NOT NULL, "is_active" TEXT NOT NULL, "is_superuser" TEXT NOT NULL)''')
24+
# ### end Raystack commands ###
25+
26+
27+
def downgrade():
28+
"""Revert migration."""
29+
# ### commands auto generated by Raystack ###
30+
op.execute('DROP TABLE IF EXISTS groups_groupmodel')
31+
op.execute('DROP TABLE IF EXISTS users_usermodel')
32+
# ### end Raystack commands ###
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Migration: None
2+
3+
Revision ID: d692853a9509
4+
Revises:
5+
Create Date: 2025-08-31 07:24:16
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'd692853a9509'
14+
down_revision = None
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
"""Apply migration."""
21+
# ### commands auto generated by Raystack ###
22+
op.execute('''CREATE TABLE IF NOT EXISTS groups_groupmodel ("id" TEXT PRIMARY KEY, "name" TEXT UNIQUE NOT NULL, "description" TEXT NOT NULL)''')
23+
op.execute('''CREATE TABLE IF NOT EXISTS users_usermodel ("id" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "age" TEXT NOT NULL, "email" TEXT NOT NULL, "password_hash" TEXT NOT NULL, "group" TEXT NOT NULL, "organization" TEXT NOT NULL, "is_active" TEXT NOT NULL, "is_superuser" TEXT NOT NULL)''')
24+
# ### end Raystack commands ###
25+
26+
27+
def downgrade():
28+
"""Revert migration."""
29+
# ### commands auto generated by Raystack ###
30+
op.execute('DROP TABLE IF EXISTS groups_groupmodel')
31+
op.execute('DROP TABLE IF EXISTS users_usermodel')
32+
# ### end Raystack commands ###

src/raystack/contrib/admin/urls.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ async def profile_view(request: Request):
110110
@router.get("/users/edit/{user_id}", response_model=None)
111111
@login_required(["user_auth"])
112112
async def user_edit_view(request: Request, user_id: int):
113-
user = UserModel.objects.filter(id=user_id).first()
114-
groups = GroupModel.objects.all()
113+
user = await UserModel.objects.filter(id=user_id).first()
114+
groups = await GroupModel.objects.all().execute()
115115
return render_template(request=request, template_name="admin/user_edit.html", context={
116116
"user": user,
117117
"groups": groups,
@@ -123,18 +123,18 @@ async def user_edit_view(request: Request, user_id: int):
123123
@login_required(["user_auth"])
124124
async def user_edit_post(request: Request, user_id: int):
125125
form = await request.form()
126-
user = UserModel.objects.filter(id=user_id).first()
126+
user = await UserModel.objects.filter(id=user_id).first()
127127
if user:
128128
user.name = form.get("name")
129129
user.age = int(form.get("age"))
130130
user.email = form.get("email")
131131
user.organization = form.get("organization")
132132
group_id = int(form.get("group_id"))
133133
user.group = group_id
134-
user.save()
134+
await user.save()
135135
return render_template(request=request, template_name="admin/user_edit.html", context={
136136
"user": user,
137-
"groups": GroupModel.objects.all(),
137+
"groups": await GroupModel.objects.all().execute(),
138138
"url_for": url_for,
139139
"config": request.app.settings,
140140
"success": True
@@ -143,7 +143,7 @@ async def user_edit_post(request: Request, user_id: int):
143143
@router.get("/groups/edit/{group_id}", response_model=None)
144144
@login_required(["user_auth"])
145145
async def group_edit_view(request: Request, group_id: int):
146-
group = GroupModel.objects.filter(id=group_id).first()
146+
group = await GroupModel.objects.filter(id=group_id).first()
147147
return render_template(request=request, template_name="admin/group_edit.html", context={
148148
"group": group,
149149
"url_for": url_for,
@@ -154,11 +154,11 @@ async def group_edit_view(request: Request, group_id: int):
154154
@login_required(["user_auth"])
155155
async def group_edit_post(request: Request, group_id: int):
156156
form = await request.form()
157-
group = GroupModel.objects.filter(id=group_id).first()
157+
group = await GroupModel.objects.filter(id=group_id).first()
158158
if group:
159159
group.name = form.get("name")
160160
group.description = form.get("description")
161-
group.save()
161+
await group.save()
162162
return render_template(request=request, template_name="admin/group_edit.html", context={
163163
"group": group,
164164
"url_for": url_for,
@@ -170,7 +170,7 @@ async def group_edit_post(request: Request, group_id: int):
170170
@router.get("/users/create", response_model=None)
171171
@login_required(["user_auth"])
172172
async def user_create_view(request: Request):
173-
groups = GroupModel.objects.all()
173+
groups = await GroupModel.objects.all().execute()
174174
return render_template(request=request, template_name="admin/user_create.html", context={
175175
"groups": groups,
176176
"url_for": url_for,
@@ -189,9 +189,9 @@ async def user_create_post(request: Request):
189189
group=int(form.get("group_id")),
190190
organization=form.get("organization")
191191
)
192-
user.save()
192+
await user.save()
193193
return render_template(request=request, template_name="admin/user_create.html", context={
194-
"groups": GroupModel.objects.all(),
194+
"groups": await GroupModel.objects.all().execute(),
195195
"url_for": url_for,
196196
"config": request.app.settings,
197197
"success": True
@@ -201,7 +201,7 @@ async def user_create_post(request: Request):
201201
@router.get("/users/delete/{user_id}", response_model=None)
202202
@login_required(["user_auth"])
203203
async def user_delete_confirm(request: Request, user_id: int):
204-
user = UserModel.objects.filter(id=user_id).first()
204+
user = await UserModel.objects.filter(id=user_id).first()
205205
return render_template(request=request, template_name="admin/user_delete.html", context={
206206
"user": user,
207207
"url_for": url_for,
@@ -211,9 +211,9 @@ async def user_delete_confirm(request: Request, user_id: int):
211211
@router.post("/users/delete/{user_id}", response_model=None)
212212
@login_required(["user_auth"])
213213
async def user_delete_post(request: Request, user_id: int):
214-
user = UserModel.objects.filter(id=user_id).first()
214+
user = await UserModel.objects.filter(id=user_id).first()
215215
if user:
216-
user.delete()
216+
await user.delete()
217217
# Redirect to users list after deletion
218218
return render_template(request=request, template_name="admin/user_delete.html", context={
219219
"deleted": True,
@@ -242,7 +242,7 @@ async def group_create_post(request: Request):
242242
name=form.cleaned_data["name"],
243243
description=form.cleaned_data["description"]
244244
)
245-
group.save()
245+
await group.save()
246246
return render_template(request=request, template_name="admin/group_create.html", context={
247247
"form": GroupCreateForm(),
248248
"url_for": url_for,
@@ -260,7 +260,7 @@ async def group_create_post(request: Request):
260260
@router.get("/groups/delete/{group_id}", response_model=None)
261261
@login_required(["user_auth"])
262262
async def group_delete_confirm(request: Request, group_id: int):
263-
group = GroupModel.objects.filter(id=group_id).first()
263+
group = await GroupModel.objects.filter(id=group_id).first()
264264
return render_template(request=request, template_name="admin/group_delete.html", context={
265265
"group": group,
266266
"url_for": url_for,
@@ -270,9 +270,9 @@ async def group_delete_confirm(request: Request, group_id: int):
270270
@router.post("/groups/delete/{group_id}", response_model=None)
271271
@login_required(["user_auth"])
272272
async def group_delete_post(request: Request, group_id: int):
273-
group = GroupModel.objects.filter(id=group_id).first()
273+
group = await GroupModel.objects.filter(id=group_id).first()
274274
if group:
275-
group.delete()
275+
await group.delete()
276276
return render_template(request=request, template_name="admin/group_delete.html", context={
277277
"deleted": True,
278278
"url_for": url_for,

0 commit comments

Comments
 (0)