Compare commits
No commits in common. "0da1a5005881aba2661381c59ce2f43521c1f1f2" and "331971da815da7abb3e1490084ef79cda4548b19" have entirely different histories.
0da1a50058
...
331971da81
46 changed files with 0 additions and 5466 deletions
|
@ -1,28 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Swagger</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/api-docs/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
requestInterceptor: (request) => {
|
||||
request.headers['X-CSRFToken'] = "{{ csrf_token }}"
|
||||
return request;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,194 +0,0 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "R/AGB",
|
||||
"description": "Une API pour choisir les couleurs en agb. Il existe un endpoint SSE pour être notifié des mises à jour des couleurs à l'addresse [https://agb.hackens.org/api/sse](https://agb.hackens.org/api/sse). Une description du plan d'addressage au format JSON est disponible: [/api-docs/patch.json](/api-docs/patch.json). On peut récupérer le token d'API à l'addresse: [/token](/token)."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://agb.hackens.org/api"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/values": {
|
||||
"get": {
|
||||
"summary": "Get the colors for all the channels",
|
||||
"operationId": "getValues",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of colors",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Change multiple colors",
|
||||
"description": "Change multiple colors. This request has a cooldown of 500ms.",
|
||||
"operationId": "changeColors",
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"exp",
|
||||
"scope",
|
||||
"user",
|
||||
"sub"
|
||||
]
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Colors to change",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/components/schemas/Color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Color updated successfully",
|
||||
"content": {
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "You're being rate-limited",
|
||||
"content": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/values/{id}": {
|
||||
"get": {
|
||||
"summary": "Get channel colors by ID",
|
||||
"operationId": "find color by id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "ID of color to fetch",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Color of specific channel",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Changes channel color",
|
||||
"description": "Changes channel color",
|
||||
"operationId": "changeColor",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "ID of color to fetch",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"exp",
|
||||
"scope",
|
||||
"user",
|
||||
"sub"
|
||||
]
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Color to change",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Color"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Color updated successfully",
|
||||
"content": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Color": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"red",
|
||||
"green",
|
||||
"blue"
|
||||
],
|
||||
"properties": {
|
||||
"red": {
|
||||
"type": "integer",
|
||||
"format": "int8"
|
||||
},
|
||||
"green": {
|
||||
"type": "integer",
|
||||
"format": "int8"
|
||||
},
|
||||
"blue": {
|
||||
"type": "integer",
|
||||
"format": "int8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1651
backend/Cargo.lock
generated
1651
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "ragb-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.7", features = ["macros"] }
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
dotenvy = "0.15.7"
|
||||
jsonwebtoken = "9.3.0"
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.16", features = ["sync"] }
|
||||
tower = "0.5.1"
|
||||
tower-http = { version = "0.6.1", features = ["tracing", "trace", "cors"] }
|
||||
tower_governor = { version = "0.4.2", features = ["axum"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
|
@ -1 +0,0 @@
|
|||
This is the backend of ragb. It provides a simple CRUD with in memory database.
|
|
@ -1,2 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyYWdiIiwic2NvcGUiOiJtb2RpZnkiLCJleHAiOjE3Mzk0ODY3MDd9.J8Qh12hxQ9rZFKhpjcth2uUrEhnFVH8PAVmkVgM1wIo" $@
|
|
@ -1,61 +0,0 @@
|
|||
use crate::model::DB;
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use axum_extra::{headers, typed_header::TypedHeader};
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
scope: String,
|
||||
user: String,
|
||||
is_cof: bool,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
name: String,
|
||||
pub is_cof: bool,
|
||||
}
|
||||
|
||||
fn check_token(token: &str, jwt: &str) -> Option<User> {
|
||||
let decoded_token = decode::<Claims>(
|
||||
&token,
|
||||
&DecodingKey::from_secret(jwt.as_bytes()),
|
||||
&Validation::new(Algorithm::HS256),
|
||||
);
|
||||
match decoded_token {
|
||||
Ok(token_data) => {
|
||||
let user = token_data.claims.user;
|
||||
let is_cof =token_data.claims.is_cof;
|
||||
if token_data.claims.scope == "modify" {
|
||||
tracing::info!("Successful auth {user:?}");
|
||||
Some(User { name: user, is_cof })
|
||||
} else {
|
||||
tracing::debug!("Failed auth: {user:?} don't have modify scope");
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(err) => {tracing::debug!("Failed decoding token: {err:?}"); None},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn jwt_middleware(
|
||||
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) {
|
||||
request.extensions_mut().insert(user);
|
||||
Ok(next.run(request).await)
|
||||
} else {
|
||||
Err(StatusCode::FORBIDDEN)
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
use crate::authorization::User;
|
||||
use crate::model::{ColorArray, DMXAtom, DMXBeam, DMXBeamChange, DMXColorAtom, DMXColor, DB};
|
||||
use axum::{
|
||||
debug_handler,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{sse::Event, IntoResponse, Sse},
|
||||
Extension, Json,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_stream::{self as stream};
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn healthcheck_handler() -> impl IntoResponse {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn list_values_handler(State(db): State<DB>) -> impl IntoResponse {
|
||||
let val;
|
||||
{
|
||||
let lock = db.mut_state.read().await;
|
||||
val = lock.dmx.clone();
|
||||
}
|
||||
Json(val)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn batch_edit_value_handler(
|
||||
State(db): State<DB>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(body): Json<Vec<DMXColorAtom>>,
|
||||
) -> Result<(), StatusCode> {
|
||||
let mut lock = db.mut_state.write().await;
|
||||
if lock.ratelimit_info.get(&user).map(|d| d.elapsed() <= Duration::from_millis(500)).unwrap_or(false){
|
||||
return Err(StatusCode::TOO_MANY_REQUESTS);
|
||||
}
|
||||
for i in &body {
|
||||
check_id(i.address, &lock.dmx.colors)?;
|
||||
}
|
||||
for i in &body {
|
||||
lock.dmx.colors[i.address] = i.value;
|
||||
match db.static_state.change_channel.send(DMXAtom::Color(*i)) {
|
||||
Ok(_) => (),
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
lock.ratelimit_info.insert(user, Instant::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_value_handler(
|
||||
Path(id): Path<usize>,
|
||||
State(db): State<DB>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let lock = db.mut_state.read().await;
|
||||
check_id(id, &lock.dmx.colors)?;
|
||||
Ok(Json(lock.dmx.colors[id]))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn edit_value_handler(
|
||||
Path(id): Path<usize>,
|
||||
State(db): State<DB>,
|
||||
Json(body): Json<DMXColor>,
|
||||
) -> Result<(), StatusCode> {
|
||||
let mut lock = db.mut_state.write().await;
|
||||
check_id(id, &lock.dmx.colors)?;
|
||||
lock.dmx.colors[id] = body;
|
||||
match db
|
||||
.static_state
|
||||
.change_channel
|
||||
.send(DMXAtom::Color(DMXColorAtom::new(id, body)))
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(_) => (),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn get_motor_value_handler(
|
||||
State(db): State<DB>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let lock = db.mut_state.read().await;
|
||||
Ok(Json(lock.dmx.motor))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn edit_motor_value_handler(
|
||||
State(db): State<DB>,
|
||||
Extension(user): Extension<User>,
|
||||
Json(body): Json<DMXBeamChange>,
|
||||
) -> Result<(), StatusCode> {
|
||||
if !user.is_cof {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
let mut lock = db.mut_state.write().await;
|
||||
lock.dmx.motor = DMXBeam {
|
||||
pan: body.pan.unwrap_or(lock.dmx.motor.pan),
|
||||
tilt: body.tilt.unwrap_or(lock.dmx.motor.tilt),
|
||||
focus: body.focus.unwrap_or(lock.dmx.motor.focus),
|
||||
};
|
||||
let _ = db
|
||||
.static_state
|
||||
.change_channel
|
||||
.send(DMXAtom::Motor(lock.dmx.motor))
|
||||
;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn sse_handler(State(db): State<DB>) -> impl IntoResponse {
|
||||
let rx = db.static_state.change_channel.subscribe();
|
||||
let data: Vec<DMXColor>;
|
||||
let motor: DMXBeam;
|
||||
{
|
||||
let lock = db.mut_state.read().await;
|
||||
data = lock.dmx.colors.clone();
|
||||
motor = lock.dmx.motor.clone();
|
||||
}
|
||||
let init_data = data.into_iter().enumerate().map(|(i, x)| {
|
||||
Ok(DMXAtom::Color(DMXColorAtom {
|
||||
address: i,
|
||||
value: x,
|
||||
}))
|
||||
}).chain(std::iter::once(Ok(DMXAtom::Motor(motor))));
|
||||
let init = stream::iter(init_data);
|
||||
let stream = init
|
||||
.chain(stream::wrappers::BroadcastStream::new(rx))
|
||||
.filter_map(|item| match item {
|
||||
Ok(val) => Some(Event::default().json_data(val)),
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
Sse::new(stream).keep_alive(
|
||||
axum::response::sse::KeepAlive::new()
|
||||
.interval(Duration::from_secs(25))
|
||||
.text("ping"),
|
||||
)
|
||||
}
|
||||
|
||||
fn check_id(id: usize, val: &ColorArray) -> Result<(), StatusCode> {
|
||||
if id >= val.len() {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
};
|
||||
Ok(())
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
mod authorization;
|
||||
mod handler;
|
||||
mod model;
|
||||
mod route;
|
||||
|
||||
use dotenvy;
|
||||
use route::create_router;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
"info,axum::rejection=trace".into()
|
||||
}),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
match dotenvy::dotenv() {
|
||||
Err(_) => tracing::info!(".env file not found"),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let binding = dotenvy::var("BIND").unwrap_or(String::from("127.0.0.1:9999"));
|
||||
let bind = binding.trim();
|
||||
tracing::debug!("Trying to bind at {bind}");
|
||||
let listener = tokio::net::TcpListener::bind(bind).await.unwrap();
|
||||
let app = create_router()
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
);
|
||||
tracing::info!("🚀 Server started successfully");
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
use crate::authorization::User;
|
||||
use dotenvy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Copy, Clone)]
|
||||
pub struct DMXColor {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
pub blue: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Copy, Clone)]
|
||||
pub struct DMXBeam {
|
||||
pub pan: u8,
|
||||
pub tilt: u8,
|
||||
pub focus: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Copy, Clone)]
|
||||
pub struct DMXBeamChange {
|
||||
pub pan: Option<u8>,
|
||||
pub tilt: Option<u8>,
|
||||
pub focus: Option<u8>,
|
||||
}
|
||||
|
||||
pub type ColorArray = Vec<DMXColor>;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum DMXAtom {
|
||||
Color(DMXColorAtom),
|
||||
Motor(DMXBeam),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
|
||||
pub struct DMXColorAtom {
|
||||
pub address: usize,
|
||||
pub value: DMXColor,
|
||||
}
|
||||
|
||||
impl DMXColorAtom {
|
||||
pub fn new(address: usize, value: DMXColor) -> DMXColorAtom {
|
||||
DMXColorAtom { address, value }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct DMXState {
|
||||
pub colors: ColorArray,
|
||||
pub motor: DMXBeam,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub dmx: DMXState,
|
||||
pub ratelimit_info: HashMap<User, Instant>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(size: usize, save_path: &str) -> AppState {
|
||||
let data: Result<DMXState, ()> =
|
||||
match fs::File::open(save_path).map(std::io::BufReader::new) {
|
||||
Ok(read) => serde_json::from_reader(read)
|
||||
.map(|mut v: DMXState| {
|
||||
v.colors.resize(
|
||||
size,
|
||||
DMXColor {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
},
|
||||
);
|
||||
v
|
||||
})
|
||||
.map_err(|e| {
|
||||
tracing::debug!("Error loading data: {e}");
|
||||
}),
|
||||
Err(e) => {
|
||||
tracing::debug!("Error loading data: {e}");
|
||||
Err(())
|
||||
}
|
||||
};
|
||||
let dmx = data.unwrap_or(DMXState {
|
||||
colors: vec![DMXColor::default(); size],
|
||||
motor: DMXBeam::default(),
|
||||
});
|
||||
|
||||
AppState {
|
||||
dmx,
|
||||
ratelimit_info: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StaticState {
|
||||
pub jwt_key: String,
|
||||
pub change_channel: broadcast::Sender<DMXAtom>,
|
||||
pub save_path: String,
|
||||
}
|
||||
|
||||
pub struct SharedState {
|
||||
pub static_state: StaticState,
|
||||
pub mut_state: RwLock<AppState>,
|
||||
}
|
||||
|
||||
pub type DB = Arc<SharedState>;
|
||||
|
||||
pub fn make_db() -> DB {
|
||||
let state_size: usize =
|
||||
usize::from_str(&dotenvy::var("NB_DMX_VALUES").unwrap_or(String::from("100"))).unwrap();
|
||||
let save_path = dotenvy::var("BK_FILE").unwrap_or(String::from("data.json"));
|
||||
let mut_state = RwLock::new(AppState::new(state_size, &save_path));
|
||||
Arc::new(SharedState {
|
||||
static_state: StaticState {
|
||||
jwt_key: serde_json::from_str(
|
||||
dotenvy::var("JWT_SECRET")
|
||||
.unwrap_or(String::from("\"secret\""))
|
||||
.as_ref(),
|
||||
)
|
||||
.expect("an JSON string"),
|
||||
change_channel: broadcast::Sender::new(512),
|
||||
save_path,
|
||||
},
|
||||
mut_state,
|
||||
})
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
use crate::authorization::jwt_middleware;
|
||||
use crate::handler;
|
||||
use crate::model;
|
||||
use axum::{handler::Handler, middleware};
|
||||
use axum::{routing::get, Router};
|
||||
use serde_json::to_writer;
|
||||
use std::fs::File;
|
||||
use tokio::task;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
pub fn create_router() -> Router {
|
||||
let db = model::make_db();
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
let db_to_save = db.clone();
|
||||
task::spawn(async move {
|
||||
loop {
|
||||
sleep(Duration::from_millis(1000)).await;
|
||||
{
|
||||
let save_path = &db_to_save.static_state.save_path;
|
||||
let file = File::create(save_path);
|
||||
match file {
|
||||
Ok(f) => {
|
||||
let db = db_to_save.mut_state.read().await;
|
||||
match to_writer(f, &db.dmx) {
|
||||
Ok(()) => tracing::trace!("Saved data at {save_path}"),
|
||||
Err(e) => {
|
||||
tracing::debug!("Failed to save data: {e:?}");
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("Failed to save data: {e:?}");
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Router::new()
|
||||
.route("/api/healthcheck", get(handler::healthcheck_handler))
|
||||
.route(
|
||||
"/api/values",
|
||||
get(handler::list_values_handler).post(
|
||||
handler::batch_edit_value_handler
|
||||
.layer(middleware::from_fn_with_state(db.clone(), jwt_middleware)),
|
||||
),
|
||||
)
|
||||
.route("/api/sse", get(handler::sse_handler))
|
||||
.route(
|
||||
"/api/values/:id",
|
||||
get(handler::get_value_handler).post(
|
||||
handler::edit_value_handler
|
||||
.layer(middleware::from_fn_with_state(db.clone(), jwt_middleware)),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/api/motor",
|
||||
get(handler::get_motor_value_handler).post(
|
||||
handler::edit_motor_value_handler
|
||||
.layer(middleware::from_fn_with_state(db.clone(), jwt_middleware)),
|
||||
),
|
||||
)
|
||||
.layer(cors)
|
||||
.with_state(db)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "frontend"
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,116 +0,0 @@
|
|||
function hexToRgb(hex) {
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
function componentToHex(c) {
|
||||
var hex = c.toString(16);
|
||||
return hex.length == 1 ? "0" + hex : hex;
|
||||
}
|
||||
|
||||
function rgbToHex({red, green, blue}) {
|
||||
return "#" + componentToHex(red) + componentToHex(green) + componentToHex(blue);
|
||||
}
|
||||
|
||||
let startSocket = () => {
|
||||
const socket = new EventSource(`${WEBSOCKET_ENDPOINT}/sse`);
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
const color = rgbToHex(message.value);
|
||||
console.log(color, `light${message.address}`);
|
||||
const element = document.getElementById(`light${message.address}`);
|
||||
if(element !== null) {
|
||||
element.style.fill = color;
|
||||
}
|
||||
const inputElement = document.getElementById(`input-light${message.address}`);
|
||||
if(inputElement !== null) {
|
||||
inputElement.value = color;
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("error", (event) => {
|
||||
console.error(event);
|
||||
startSocket();
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', startSocket);
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Functions to open and close a modal
|
||||
function openModal($el) {
|
||||
$el.showModal();
|
||||
}
|
||||
|
||||
function closeModal($el) {
|
||||
$el.close();
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('dialog') || []).forEach(($modal) => {
|
||||
closeModal($modal);
|
||||
});
|
||||
}
|
||||
|
||||
const $loadingModal = document.getElementById("modal-loading");
|
||||
closeModal($loadingModal);
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.modal-trigger') || []).forEach(($trigger) => {
|
||||
const modal = $trigger.dataset.target;
|
||||
const $target = document.getElementById(modal);
|
||||
|
||||
if ($target !== null) $trigger.addEventListener('click', () => {
|
||||
openModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.close-modal') || []).forEach(($close) => {
|
||||
const $target = $close.closest('dialog');
|
||||
|
||||
$close.addEventListener('click', () => {
|
||||
closeModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Add a click event to save colors
|
||||
(document.querySelectorAll('.save-color') || []).forEach(($trigger) => {
|
||||
const lightId = $trigger.dataset.target;
|
||||
const $input = document.getElementById(`input-light${lightId}`);
|
||||
const url = `${WEBSOCKET_ENDPOINT}/values/${lightId}`;
|
||||
|
||||
$trigger.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const color = hexToRgb($input.value);
|
||||
openModal($loadingModal)
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
//mode: "no-cors",
|
||||
headers: {
|
||||
'Accept': "application/json",
|
||||
'Content-Type': "application/json",
|
||||
'Authorization': `Bearer ${JWT}`
|
||||
},
|
||||
body: JSON.stringify({"red": color.r, "green": color.g, "blue": color.b}),
|
||||
}).then((resp) => {
|
||||
if(!resp.ok) {
|
||||
alert(`Request failed. Err code:${resp.status}`);
|
||||
}
|
||||
}).catch((e) => {
|
||||
alert(`Request failed. Err: ${e}`);
|
||||
console.log(e);
|
||||
}).finally(() => {
|
||||
closeAllModals();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
|
Before Width: | Height: | Size: 410 B |
|
@ -1,60 +0,0 @@
|
|||
{% load static %}
|
||||
<!doctype html>
|
||||
<html data-theme="light" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<link rel="stylesheet" href="{% static "css/pico.pumpkin.min.css" %}">
|
||||
<title>R/AGB</title>
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'media/logo.png' %}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
<a aria-label="Page d'accueil" href="{% url "frontend:home" %}">
|
||||
<img style="height:50px;" src="{% static "media/logo.png" %}" alt="Logo d'hackens"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="hide-before-sm">
|
||||
<a class="contrast" href="{% url "frontend:home" %}">Accueil</a>
|
||||
</li>
|
||||
<li class="hide-before-sm">
|
||||
<a class="contrast" href="/api-docs/">API</a>
|
||||
</li>
|
||||
{% if user.is_staff %}
|
||||
<li class="hide-before-sm">
|
||||
<a class="contrast" href="{% url "admin:index" %}">Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url "authens:logout" %}">
|
||||
<strong>Déconnexion</strong>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button>
|
||||
🧑 {{ user.username }}
|
||||
</button>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{% url "authens:login" %}">
|
||||
Connexion
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
|
@ -1,63 +0,0 @@
|
|||
{% extends "frontend/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
{% if not user.is_authenticated %}
|
||||
<article>
|
||||
<p>
|
||||
Connectez-vous pour pouvoir modifier des couleurs
|
||||
</p>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h1>{{ light_name }}</h1>
|
||||
<figure>
|
||||
<svg viewBox="-3.5 -3.5 112 112" version="1.1" id="blinder" >
|
||||
<style>
|
||||
circle {
|
||||
fill: white;
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
}
|
||||
{% if user.is_authenticated %}
|
||||
circle:hover {
|
||||
stroke: grey;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
<g id="layer1">
|
||||
<rect style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" width="110" height="110" x="-2.5" y="-2.5" rx="15" />
|
||||
{% for light in lights %}
|
||||
<circle id="light{{ light.id }}" r="10" cx="{{ light.position_x }}" cy="{{ light.position_y }}" data-target="modal-light{{ light.id }}" class="modal-trigger"/>
|
||||
{% endfor %}
|
||||
</g>
|
||||
</svg>
|
||||
</figure>
|
||||
</section>
|
||||
{% if user.is_authenticated %}
|
||||
{% for light in lights %}
|
||||
<dialog id="modal-light{{light.id}}">
|
||||
<article>
|
||||
<header>
|
||||
<button class="close-modal" rel="prev" aria-label="close"></button>
|
||||
<h3>Couleur du pixel n°{{light.id}}</h3>
|
||||
</header>
|
||||
<section>
|
||||
<form>
|
||||
<input id="input-light{{light.id}}" class="input" type="color" name="light{{light.id}}">
|
||||
<button class="save-color" data-target="{{light.id}}">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<dialog open id="modal-loading">
|
||||
<article aria-busy="true"></article>
|
||||
</dialog>
|
||||
<script>const WEBSOCKET_ENDPOINT = "{{ websocket_endpoint }}";</script>
|
||||
{% if user.is_authenticated %}
|
||||
<script>const JWT = "{{ jwt }}";</script>
|
||||
{% endif %}
|
||||
<script src="{% static "js/update-color.js" %}" defer></script>
|
||||
{% endblock %}
|
File diff suppressed because one or more lines are too long
|
@ -1,54 +0,0 @@
|
|||
{% extends "frontend/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<section class="section content container is-max-desktop">
|
||||
<h1 class="title is-3">{{ object.name }}</h1>
|
||||
<svg viewBox="-1 -1 162 12" version="1.1" id="barreled" >
|
||||
<style>
|
||||
rect {
|
||||
fill: white;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
}
|
||||
{% if user.is_authenticated %}
|
||||
rect:hover {
|
||||
stroke: grey;
|
||||
}
|
||||
{% endif %}
|
||||
</style>
|
||||
<g id="layer1">
|
||||
{% for light in lights %}
|
||||
<rect id="light{{ light.id }}" width="10" height="10" x="{{ light.position }}" y="0" class="modal-trigger" data-target="modal-light{{ light.id }}" />
|
||||
{% endfor %}
|
||||
</g>
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{% for light in lights %}
|
||||
<dialog id="modal-light{{light.id}}">
|
||||
<article>
|
||||
<header>
|
||||
<button class="close-modal" rel="prev" aria-label="close"></button>
|
||||
<h3>Couleur du pixel n°{{light.id}}</h3>
|
||||
</header>
|
||||
<section>
|
||||
<form>
|
||||
<input id="input-light{{light.id}}" class="input" type="color" name="light{{light.id}}">
|
||||
<button class="save-color" data-target="{{light.id}}">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<dialog open id="modal-loading">
|
||||
<article aria-busy="true"></article>
|
||||
</dialog>
|
||||
<script>const WEBSOCKET_ENDPOINT = "{{ websocket_endpoint }}";</script>
|
||||
{% if user.is_authenticated %}
|
||||
<script>const JWT = "{{ jwt }}";</script>
|
||||
{% endif %}
|
||||
<script src="{% static "js/update-color.js" %}" defer></script>
|
||||
{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||
{% extends "frontend/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
{% if not user.is_authenticated %}
|
||||
<article>
|
||||
<p>
|
||||
Connectez-vous pour pouvoir modifier des couleurs
|
||||
</p>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<section>
|
||||
<h1>{{ light_name }}</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<form>
|
||||
<input id="input-light{{lights.id}}" class="input" type="color" name="light{{lights.id}}">
|
||||
<button class="save-color" data-target="{{lights.id}}">Enregistrer</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
<dialog open id="modal-loading">
|
||||
<article aria-busy="true"></article>
|
||||
</dialog>
|
||||
<script>const WEBSOCKET_ENDPOINT = "{{ websocket_endpoint }}";</script>
|
||||
{% if user.is_authenticated %}
|
||||
<script>const JWT = "{{ jwt }}";</script>
|
||||
{% endif %}
|
||||
<script src="{% static "js/update-color.js" %}" defer></script>
|
||||
{% endblock %}
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,10 +0,0 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import HomeView, LightView, TokenView
|
||||
|
||||
app_name = "frontend"
|
||||
urlpatterns = [
|
||||
path("", HomeView.as_view(), name="home"),
|
||||
path("light/<str:light>/", LightView.as_view()),
|
||||
path("token", TokenView.as_view()),
|
||||
]
|
|
@ -1,96 +0,0 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ViewDoesNotExist
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.views import View
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
|
||||
def get_context_from_proj(kind, chans):
|
||||
print(kind, chans)
|
||||
match kind:
|
||||
case "blinder":
|
||||
return [
|
||||
{
|
||||
"id": chans[i],
|
||||
"position_y": ((15 - i) // 4) * 25 + 15,
|
||||
"position_x": ((15 - i) % 4) * 25 + 15,
|
||||
}
|
||||
for i in range(len(chans))
|
||||
]
|
||||
case "led_tub":
|
||||
return [
|
||||
{
|
||||
"id": chans[i],
|
||||
"position": i * 10,
|
||||
}
|
||||
for i in range(len(chans))
|
||||
]
|
||||
case "spot":
|
||||
return {
|
||||
"id": chans[0],
|
||||
}
|
||||
|
||||
case _:
|
||||
raise ViewDoesNotExist()
|
||||
|
||||
|
||||
class TokenView(LoginRequiredMixin, View):
|
||||
def get(self, request, *arg, **kwargs):
|
||||
return JsonResponse(
|
||||
{
|
||||
"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):
|
||||
def get_template_names(self):
|
||||
lights = settings.LIGHTS["lights"][self.kwargs["light"]]
|
||||
return [f"frontend/{lights['kind']}.html"]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.user.is_authenticated:
|
||||
context["jwt"] = jwt.encode(
|
||||
{
|
||||
"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
|
||||
light = self.kwargs["light"]
|
||||
if light not in settings.LIGHTS["lights"]:
|
||||
raise Http404("Light does not exist")
|
||||
lights = settings.LIGHTS["lights"][light]
|
||||
context["lights"] = get_context_from_proj(lights["kind"], lights["channels"])
|
||||
context["light_name"] = lights["name"]
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
template_name = "frontend/home.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["websocket_endpoint"] = settings.WEBSOCKET_ENDPOINT
|
||||
lights = settings.LIGHTS["lights"]
|
||||
context["lights"] = lights
|
||||
|
||||
return context
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ragb.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"lights": {
|
||||
"blinder": {
|
||||
"name": "Blinder",
|
||||
"kind": "blinder",
|
||||
"channels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||
},
|
||||
"screen_left": {
|
||||
"name": "Ecran-gauche",
|
||||
"kind": "spot",
|
||||
"channels": [ 16 ]
|
||||
},
|
||||
"screen_right": {
|
||||
"name": "Ecran-droite",
|
||||
"kind": "spot",
|
||||
"channels": [ 17 ]
|
||||
},
|
||||
"wall_left": {
|
||||
"name": "Mur gauche",
|
||||
"kind": "spot",
|
||||
"channels": [ 34 ]
|
||||
},
|
||||
"wall_right": {
|
||||
"name": "Mur droite",
|
||||
"kind": "spot",
|
||||
"channels": [ 35 ]
|
||||
},
|
||||
"barre_led_0": {
|
||||
"name": "Barre led 1",
|
||||
"kind": "led_tub",
|
||||
"channels" : [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
|
||||
},
|
||||
"barre_led_1": {
|
||||
"name": "Barre led 2",
|
||||
"kind": "led_tub",
|
||||
"channels" : [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
|
||||
},
|
||||
"barre_led_2": {
|
||||
"name": "Barre led 3",
|
||||
"kind": "led_tub",
|
||||
"channels" : [52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67]
|
||||
},
|
||||
"barre_led_3": {
|
||||
"name": "Barre led 4",
|
||||
"kind": "led_tub",
|
||||
"channels" : [68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
ASGI config for ragb project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ragb.settings")
|
||||
|
||||
application = get_asgi_application()
|
|
@ -1,196 +0,0 @@
|
|||
"""
|
||||
Django settings for ragb project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-elz+_!5$ad6r%%_aia_8pdzdrd(i=d_krcd@gv1t11wolz@bv^"
|
||||
|
||||
JWT_SECRET = "secret"
|
||||
|
||||
DEV_KEY = "supersecret"
|
||||
|
||||
# Custom settings
|
||||
|
||||
WEBSOCKET_PORT = 9999
|
||||
WEBSOCKET_HOST = "127.0.0.1"
|
||||
WEBSOCKET_ENDPOINT = f"http://{WEBSOCKET_HOST}:{WEBSOCKET_PORT}/api"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = "127.0.0.1,localhost".split(",")
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"authens",
|
||||
"frontend",
|
||||
"shared",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "ragb.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "ragb.wsgi.application"
|
||||
|
||||
# AuthENS
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"authens.backends.ENSCASBackend",
|
||||
]
|
||||
|
||||
AUTHENS_ALLOW_STAFF = True
|
||||
AUTHENS_USE_OLDCAS = False
|
||||
|
||||
|
||||
LOGIN_URL = reverse_lazy("authens:login")
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy("frontend:home")
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy("frontend:home")
|
||||
|
||||
# DJDT
|
||||
|
||||
|
||||
# DEBUG_TOOLBAR_CONFIG = {
|
||||
# # Toolbar options
|
||||
# "PROFILER_MAX_DEPTH": 20,
|
||||
# "SHOW_TOOLBAR_CALLBACK": "ragb.utils.show_debug_toolbar",
|
||||
# }
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
"HOST": "",
|
||||
"PORT": "",
|
||||
},
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = (
|
||||
[
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
if not DEBUG
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# logging
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["console"],
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
STATIC_ROOT = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
LIGHTS = json.load(open(BASE_DIR / "patch.json"))
|
|
@ -1,26 +0,0 @@
|
|||
"""
|
||||
URL configuration for ragb project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("authens/", include("authens.urls")),
|
||||
path("", include("frontend.urls")),
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for ragb project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ragb.settings")
|
||||
|
||||
application = get_wsgi_application()
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SharedConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'shared'
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,80 +0,0 @@
|
|||
# Generated by npins. Do not modify; will be overwritten regularly
|
||||
let
|
||||
data = builtins.fromJSON (builtins.readFile ./sources.json);
|
||||
version = data.version;
|
||||
|
||||
mkSource =
|
||||
spec:
|
||||
assert spec ? type;
|
||||
let
|
||||
path =
|
||||
if spec.type == "Git" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "GitRelease" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "PyPi" then
|
||||
mkPyPiSource spec
|
||||
else if spec.type == "Channel" then
|
||||
mkChannelSource spec
|
||||
else
|
||||
builtins.throw "Unknown source type ${spec.type}";
|
||||
in
|
||||
spec // { outPath = path; };
|
||||
|
||||
mkGitSource =
|
||||
{
|
||||
repository,
|
||||
revision,
|
||||
url ? null,
|
||||
hash,
|
||||
branch ? null,
|
||||
...
|
||||
}:
|
||||
assert repository ? type;
|
||||
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
|
||||
# In the latter case, there we will always be an url to the tarball
|
||||
if url != null then
|
||||
(builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash; # FIXME: check nix version & use SRI hashes
|
||||
})
|
||||
else
|
||||
assert repository.type == "Git";
|
||||
let
|
||||
urlToName =
|
||||
url: rev:
|
||||
let
|
||||
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
|
||||
|
||||
short = builtins.substring 0 7 rev;
|
||||
|
||||
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
|
||||
in
|
||||
"${if matched == null then "source" else builtins.head matched}${appendShort}";
|
||||
name = urlToName repository.url revision;
|
||||
in
|
||||
builtins.fetchGit {
|
||||
url = repository.url;
|
||||
rev = revision;
|
||||
inherit name;
|
||||
# hash = hash;
|
||||
};
|
||||
|
||||
mkPyPiSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchurl {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkChannelSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
in
|
||||
if version == 3 then
|
||||
builtins.mapAttrs (_: mkSource) data.pins
|
||||
else
|
||||
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"pins": {
|
||||
"nixpkgs": {
|
||||
"type": "Channel",
|
||||
"name": "nixpkgs-unstable",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre690413.1366d1af8f58/nixexprs.tar.xz",
|
||||
"hash": "0nlrrs22pf3dr7m55glxxfdg2qn6q9sjsd5dc7bvigasb1v3gw4j"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{ lib, fetchgit, pythoncas, django, ldap, buildPythonPackage }:
|
||||
buildPythonPackage rec {
|
||||
pname = "authens";
|
||||
version = "v0.1b5";
|
||||
doCheck = false;
|
||||
src = fetchgit {
|
||||
url = "https://git.eleves.ens.fr/klub-dev-ens/authens.git";
|
||||
rev = "58747e57b30b47f36a0ed3e7c80850ed7f1edbf9";
|
||||
hash = "sha256-R0Nw212/BOPHfpspT5wzxtji1vxZ/JOuwr00naklWE8=";
|
||||
};
|
||||
propagatedBuildInputs = [ django ldap pythoncas ];
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{ lib, requests, lxml, six, buildPythonPackage, fetchFromGitHub }:
|
||||
buildPythonPackage rec {
|
||||
pname = "python-cas";
|
||||
version = "1.6.0";
|
||||
doCheck = false;
|
||||
src = fetchFromGitHub {
|
||||
owner = "python-cas";
|
||||
repo = "python-cas";
|
||||
rev = "v1.6.0";
|
||||
sha512 = "sha512-qnYzgwELUij2EdqA6H17q8vnNUsfI7DkbZSI8CCIGfXOM+cZ7vsWe7CJxzsDUw73sBPB4+zzpLxvb7tpm/IDeg==";
|
||||
};
|
||||
propagatedBuildInputs = [ requests lxml six ];
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{ lib, python3, debug ? false }:
|
||||
let
|
||||
python = python3.override {
|
||||
packageOverrides = self: super: {
|
||||
django = super.django_4;
|
||||
authens = self.callPackage ./authens.nix { };
|
||||
pythoncas = self.callPackage ./python-cas.nix { };
|
||||
};
|
||||
};
|
||||
in
|
||||
python.withPackages (ps: [
|
||||
ps.django
|
||||
ps.gunicorn
|
||||
ps.authens
|
||||
ps.pyjwt
|
||||
] ++ lib.optionals debug [
|
||||
ps.django-debug-toolbar
|
||||
ps.black
|
||||
ps.isort
|
||||
])
|
|
@ -1,85 +0,0 @@
|
|||
#!/nix/store/z46wvcd15152qnsry80p8ricxya2n2lr-python3-3.11.7-env/bin/python
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from colour import Color
|
||||
from pyjecteur.fixtures import Blinder, LedBar48Ch, Tradi
|
||||
from pyjecteur.lights import Universe
|
||||
from pyjecteur.widget import Widget
|
||||
|
||||
if False: # True:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
w = Widget("/dev/ttyUSB0")
|
||||
|
||||
DIM = {
|
||||
"blinder": 0.10,
|
||||
"led_tub": 0.05,
|
||||
"spot": 0.3,
|
||||
}
|
||||
|
||||
u = Universe(w)
|
||||
|
||||
|
||||
def strToProj(s):
|
||||
match s:
|
||||
case "spot":
|
||||
return Tradi()
|
||||
case "led_tub":
|
||||
return LedBar48Ch()
|
||||
case "blinder":
|
||||
return Blinder()
|
||||
|
||||
|
||||
r = requests.get("https://agb.hackens.org/api-docs/patch.json")
|
||||
patch = r.json()
|
||||
|
||||
lights = {}
|
||||
|
||||
update = {}
|
||||
|
||||
current_addr = 0
|
||||
|
||||
for k, v in patch["lights"].items():
|
||||
lights[k] = strToProj(v["kind"])
|
||||
u.register(lights[k], current_addr)
|
||||
# update dmx since some params are set before
|
||||
lights[k].update_dmx()
|
||||
logging.info(
|
||||
f"Light {k} of kind {v['kind']} is at DMX{current_addr+1} (PLS convention)"
|
||||
)
|
||||
for i, chan in enumerate(v["channels"]):
|
||||
update[chan] = (k, i) # put the light name
|
||||
current_addr += lights[k].address_size
|
||||
|
||||
|
||||
def update_light(address, red, green, blue):
|
||||
light, chan = update[address]
|
||||
kind = patch["lights"][light]["kind"]
|
||||
r, g, b = red * DIM[kind]/255, (green * DIM[kind])/255, (blue * DIM[kind])/255
|
||||
match kind:
|
||||
case "blinder":
|
||||
lights[light].colors[chan] = Color(rgb=(r, g, b))
|
||||
case "led_tub":
|
||||
lights[light].colors[chan] = Color(rgb=(r, g, b))
|
||||
case "spot":
|
||||
lights[light].color = Color(rgb=(r, g, b))
|
||||
|
||||
|
||||
def run():
|
||||
logging.info("Started")
|
||||
for line in sys.stdin:
|
||||
logging.debug(line)
|
||||
if line.startswith("data:"):
|
||||
dataStr = line[5:]
|
||||
logging.debug(f"Received: {dataStr}")
|
||||
data = json.loads(dataStr)
|
||||
if data["address"] in update:
|
||||
update_light(data["address"], **data["value"])
|
||||
|
||||
run()
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{ pkgs ? import (import ../npins).nixpkgs { } }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
(pkgs.callPackage ./python.nix { debug = true; })
|
||||
pkgs.cargo
|
||||
pkgs.cargo-edit
|
||||
pkgs.rustc
|
||||
pkgs.rust-analyzer
|
||||
pkgs.rustfmt
|
||||
];
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
provisioning/shell.nix
|
Loading…
Add table
Reference in a new issue