Compare commits

...

3 commits

Author SHA1 Message Date
sinavir
dfbaf2fd65 feat(frontend/management command): Add a command to craft tokens 2024-10-12 17:37:55 +02:00
sinavir
005dc42433 fix(documentation): Update documentation 2024-10-12 17:37:27 +02:00
sinavir
899fe7f45c chore(backend): Refactor a bit authorization
Use a middleware for cof membership checking
2024-10-12 17:37:11 +02:00
9 changed files with 138 additions and 33 deletions

View file

@ -156,6 +156,60 @@
} }
} }
} }
},
"/control-box": {
"get": {
"summary": "Get control box information",
"operationId": "viewBox",
"parameters": [],
"responses": {
"200": {
"description": "Control box info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Box"
}
}
}
}
}
},
"post": {
"summary": "Change box state",
"description": "Changes box state, you need to be cof to act on this endpoint. The json do not need to contain all fields, it can be partial and only the provided values will be updated.",
"operationId": "changeBox",
"parameters": [],
"security": [
{
"bearerAuth": [
"exp",
"scope",
"user",
"is_cof",
"sub"
]
}
],
"requestBody": {
"description": "Box state",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Box"
}
}
}
},
"responses": {
"200": {
"description": "Box state updated successfully",
"content": {
}
}
}
}
} }
}, },
"components": { "components": {
@ -181,6 +235,24 @@
"format": "int8" "format": "int8"
} }
} }
},
"Box": {
"type": "object",
"required": [ ],
"properties": {
"pan": {
"type": "integer",
"format": "int8"
},
"tilt": {
"type": "integer",
"format": "int8"
},
"focus": {
"type": "integer",
"format": "int8"
}
}
} }
}, },
"securitySchemes": { "securitySchemes": {

View file

@ -59,3 +59,19 @@ pub async fn jwt_middleware(
Err(StatusCode::FORBIDDEN) Err(StatusCode::FORBIDDEN)
} }
} }
pub async fn jwt_middleware_cof(
State(state): State<DB>,
TypedHeader(auth): TypedHeader<headers::Authorization<headers::authorization::Bearer>>,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = auth.token();
if let Some(user) = check_token(token, &state.static_state.jwt_key) {
if user.is_cof {
request.extensions_mut().insert(user);
return Ok(next.run(request).await)
};
};
Err(StatusCode::FORBIDDEN)
}

View file

@ -92,12 +92,8 @@ pub async fn get_motor_value_handler(
#[debug_handler] #[debug_handler]
pub async fn edit_motor_value_handler( pub async fn edit_motor_value_handler(
State(db): State<DB>, State(db): State<DB>,
Extension(user): Extension<User>,
Json(body): Json<DMXBeamChange>, Json(body): Json<DMXBeamChange>,
) -> Result<(), StatusCode> { ) -> Result<(), StatusCode> {
if !user.is_cof {
return Err(StatusCode::FORBIDDEN);
}
let mut lock = db.mut_state.write().await; let mut lock = db.mut_state.write().await;
lock.dmx.motor = DMXBeam { lock.dmx.motor = DMXBeam {
pan: body.pan.unwrap_or(lock.dmx.motor.pan), pan: body.pan.unwrap_or(lock.dmx.motor.pan),

View file

@ -1,4 +1,4 @@
use crate::authorization::jwt_middleware; use crate::authorization::{ jwt_middleware, jwt_middleware_cof };
use crate::handler; use crate::handler;
use crate::model; use crate::model;
use axum::{handler::Handler, middleware}; use axum::{handler::Handler, middleware};
@ -61,10 +61,10 @@ pub fn create_router() -> Router {
), ),
) )
.route( .route(
"/api/motor", "/api/control-box",
get(handler::get_motor_value_handler).post( get(handler::get_motor_value_handler).post(
handler::edit_motor_value_handler handler::edit_motor_value_handler
.layer(middleware::from_fn_with_state(db.clone(), jwt_middleware)), .layer(middleware::from_fn_with_state(db.clone(), jwt_middleware_cof)),
), ),
) )
.layer(cors) .layer(cors)

View file

View file

@ -0,0 +1,22 @@
import pprint
from django.core.management.base import BaseCommand
from frontend.utils import craft_token
class Command(BaseCommand):
help = "Craft a token for the backend"
def add_arguments(self, parser):
parser.add_argument(
"--is_cof",
action="store_true",
)
parser.add_argument("user", type=str)
parser.add_argument("exp_time", type=int)
def handle(self, *args, **options):
token = craft_token(options["user"], options["is_cof"], options["exp_time"])
self.stdout.write(f"Token:\n{pprint.pformat(token)}")

View file

@ -0,0 +1,21 @@
from datetime import datetime, timedelta, timezone
import jwt
from django.conf import settings
def craft_token(username, is_cof, hours=9):
claims = {
"exp": datetime.now(tz=timezone.utc) + timedelta(hours=hours),
"sub": "ragb",
"user": username,
"is_cof": is_cof,
"scope": "modify",
}
return {
"token": jwt.encode(
claims,
settings.JWT_SECRET,
),
"claims": claims,
}

View file

@ -1,6 +1,3 @@
from datetime import datetime, timedelta, timezone
import jwt
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ViewDoesNotExist from django.core.exceptions import ViewDoesNotExist
@ -8,6 +5,8 @@ from django.http import Http404, JsonResponse
from django.views import View from django.views import View
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from .utils import craft_token
def get_context_from_proj(kind, chans): def get_context_from_proj(kind, chans):
print(kind, chans) print(kind, chans)
@ -40,20 +39,7 @@ def get_context_from_proj(kind, chans):
class TokenView(LoginRequiredMixin, View): class TokenView(LoginRequiredMixin, View):
def get(self, request, *arg, **kwargs): def get(self, request, *arg, **kwargs):
return JsonResponse( return JsonResponse(craft_token(self.request.user.username, self.request.user.groups.filter(name="cof").exists()))
{
"token": jwt.encode(
{
"exp": datetime.now(tz=timezone.utc) + timedelta(hours=9),
"sub": "ragb",
"user": self.request.user.username,
"is_cof": self.requests.user.groups.filter(name="cof").exists(),
"scope": "modify",
},
settings.JWT_SECRET,
)
}
)
class LightView(TemplateView): class LightView(TemplateView):
@ -64,15 +50,7 @@ class LightView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
context["jwt"] = jwt.encode( context["jwt"] = craft_token(self.request.user.username, self.request.user.groups.filter(name="cof").exists())["token"]
{
"exp": datetime.now(tz=timezone.utc) + timedelta(hours=9),
"sub": "ragb",
"user": self.request.user.username,
"scope": "modify",
},
settings.JWT_SECRET,
)
context["websocket_endpoint"] = settings.WEBSOCKET_ENDPOINT context["websocket_endpoint"] = settings.WEBSOCKET_ENDPOINT
light = self.kwargs["light"] light = self.kwargs["light"]
if light not in settings.LIGHTS["lights"]: if light not in settings.LIGHTS["lights"]: