init v3
This commit is contained in:
parent
331971da81
commit
3d1673d3f5
46 changed files with 5395 additions and 0 deletions
28
api-docs/index.html
Normal file
28
api-docs/index.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!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>
|
194
api-docs/openapi.json
Normal file
194
api-docs/openapi.json
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
{
|
||||||
|
"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
Normal file
1651
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
backend/Cargo.toml
Normal file
21
backend/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[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
backend/README.md
Normal file
1
backend/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the backend of ragb. It provides a simple CRUD with in memory database.
|
2
backend/auth_curl.sh
Executable file
2
backend/auth_curl.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyYWdiIiwic2NvcGUiOiJtb2RpZnkiLCJleHAiOjE3Mzk0ODY3MDd9.J8Qh12hxQ9rZFKhpjcth2uUrEhnFVH8PAVmkVgM1wIo" $@
|
56
backend/src/authorization.rs
Normal file
56
backend/src/authorization.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct User(String);
|
||||||
|
|
||||||
|
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;
|
||||||
|
if token_data.claims.scope == "modify" {
|
||||||
|
tracing::info!("Successful auth {user:?}");
|
||||||
|
Some(user)
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
118
backend/src/handler.rs
Normal file
118
backend/src/handler.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use crate::authorization::User;
|
||||||
|
use crate::model::{DMXArray, DMXAtom, 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<DMXAtom>>,
|
||||||
|
) -> 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)?;
|
||||||
|
}
|
||||||
|
for i in &body {
|
||||||
|
lock.dmx[i.address] = i.value;
|
||||||
|
match db.static_state.color_change_channel.send(*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)?;
|
||||||
|
Ok(Json(lock.dmx[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)?;
|
||||||
|
lock.dmx[id] = body;
|
||||||
|
match db
|
||||||
|
.static_state
|
||||||
|
.color_change_channel
|
||||||
|
.send(DMXAtom::new(id, body))
|
||||||
|
{
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn sse_handler(State(db): State<DB>) -> impl IntoResponse {
|
||||||
|
let rx = db.static_state.color_change_channel.subscribe();
|
||||||
|
let data: Vec<DMXColor>;
|
||||||
|
{
|
||||||
|
let lock = db.mut_state.read().await;
|
||||||
|
data = lock.dmx.clone();
|
||||||
|
}
|
||||||
|
let init_data = data.into_iter().enumerate().map(|(i, x)| {
|
||||||
|
Ok(DMXAtom {
|
||||||
|
address: i,
|
||||||
|
value: x,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
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: &DMXArray) -> Result<(), StatusCode> {
|
||||||
|
if id >= val.len() {
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
36
backend/src/main.rs
Normal file
36
backend/src/main.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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();
|
||||||
|
}
|
108
backend/src/model.rs
Normal file
108
backend/src/model.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
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, Deserialize, Serialize, Copy, Clone)]
|
||||||
|
pub struct DMXColor {
|
||||||
|
pub red: u8,
|
||||||
|
pub green: u8,
|
||||||
|
pub blue: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DMXArray = Vec<DMXColor>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Copy, Clone)]
|
||||||
|
pub struct DMXAtom {
|
||||||
|
pub address: usize,
|
||||||
|
pub value: DMXColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DMXAtom {
|
||||||
|
pub fn new(address: usize, value: DMXColor) -> DMXAtom {
|
||||||
|
DMXAtom { address, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub dmx: DMXArray,
|
||||||
|
pub ratelimit_info: HashMap<User, Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(size: usize, save_path: &str) -> AppState {
|
||||||
|
let data: Result<DMXArray, ()> =
|
||||||
|
match fs::File::open(save_path).map(std::io::BufReader::new) {
|
||||||
|
Ok(read) => serde_json::from_reader(read)
|
||||||
|
.map(|mut v: DMXArray| {
|
||||||
|
v.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(vec![
|
||||||
|
DMXColor {
|
||||||
|
red: 0,
|
||||||
|
green: 0,
|
||||||
|
blue: 0,
|
||||||
|
};
|
||||||
|
size
|
||||||
|
]);
|
||||||
|
|
||||||
|
AppState {
|
||||||
|
dmx,
|
||||||
|
ratelimit_info: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StaticState {
|
||||||
|
pub jwt_key: String,
|
||||||
|
pub color_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"),
|
||||||
|
color_change_channel: broadcast::Sender::new(512),
|
||||||
|
save_path,
|
||||||
|
},
|
||||||
|
mut_state,
|
||||||
|
})
|
||||||
|
}
|
65
backend/src/route.rs
Normal file
65
backend/src/route.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(db)
|
||||||
|
}
|
0
frontend/frontend/__init__.py
Normal file
0
frontend/frontend/__init__.py
Normal file
3
frontend/frontend/admin.py
Normal file
3
frontend/frontend/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
frontend/frontend/apps.py
Normal file
6
frontend/frontend/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "frontend"
|
3
frontend/frontend/models.py
Normal file
3
frontend/frontend/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
116
frontend/frontend/static/js/update-color.js
Normal file
116
frontend/frontend/static/js/update-color.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
BIN
frontend/frontend/static/media/logo.png
Normal file
BIN
frontend/frontend/static/media/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
1
frontend/frontend/static/media/user.svg
Normal file
1
frontend/frontend/static/media/user.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 410 B |
60
frontend/frontend/templates/frontend/base.html
Normal file
60
frontend/frontend/templates/frontend/base.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{% 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>
|
63
frontend/frontend/templates/frontend/blinder.html
Normal file
63
frontend/frontend/templates/frontend/blinder.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{% 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 %}
|
2096
frontend/frontend/templates/frontend/home.html
Normal file
2096
frontend/frontend/templates/frontend/home.html
Normal file
File diff suppressed because one or more lines are too long
54
frontend/frontend/templates/frontend/led_tub.html
Normal file
54
frontend/frontend/templates/frontend/led_tub.html
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{% 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 %}
|
29
frontend/frontend/templates/frontend/spot.html
Normal file
29
frontend/frontend/templates/frontend/spot.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% 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 %}
|
3
frontend/frontend/tests.py
Normal file
3
frontend/frontend/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
10
frontend/frontend/urls.py
Normal file
10
frontend/frontend/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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()),
|
||||||
|
]
|
95
frontend/frontend/views.py
Normal file
95
frontend/frontend/views.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
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,
|
||||||
|
"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
|
22
frontend/manage.py
Executable file
22
frontend/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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()
|
49
frontend/patch.json
Normal file
49
frontend/patch.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
frontend/ragb/__init__.py
Normal file
0
frontend/ragb/__init__.py
Normal file
16
frontend/ragb/asgi.py
Normal file
16
frontend/ragb/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
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()
|
196
frontend/ragb/settings.py
Normal file
196
frontend/ragb/settings.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
"""
|
||||||
|
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"))
|
26
frontend/ragb/urls.py
Normal file
26
frontend/ragb/urls.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""
|
||||||
|
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")),
|
||||||
|
]
|
16
frontend/ragb/wsgi.py
Normal file
16
frontend/ragb/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
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()
|
0
frontend/shared/__init__.py
Normal file
0
frontend/shared/__init__.py
Normal file
6
frontend/shared/apps.py
Normal file
6
frontend/shared/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SharedConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'shared'
|
4
frontend/shared/static/css/pico.amber.min.css
vendored
Normal file
4
frontend/shared/static/css/pico.amber.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/shared/static/css/pico.green.min.css
vendored
Normal file
4
frontend/shared/static/css/pico.green.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/shared/static/css/pico.pumpkin.min.css
vendored
Normal file
4
frontend/shared/static/css/pico.pumpkin.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
80
npins/default.nix
Normal file
80
npins/default.nix
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# 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`"
|
11
npins/sources.json
Normal file
11
npins/sources.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"pins": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"type": "Channel",
|
||||||
|
"name": "nixpkgs-unstable",
|
||||||
|
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre690413.1366d1af8f58/nixexprs.tar.xz",
|
||||||
|
"hash": "0nlrrs22pf3dr7m55glxxfdg2qn6q9sjsd5dc7bvigasb1v3gw4j"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 3
|
||||||
|
}
|
12
provisioning/authens.nix
Normal file
12
provisioning/authens.nix
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{ 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 ];
|
||||||
|
}
|
13
provisioning/python-cas.nix
Normal file
13
provisioning/python-cas.nix
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{ 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 ];
|
||||||
|
}
|
20
provisioning/python.nix
Normal file
20
provisioning/python.nix
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{ 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
|
||||||
|
])
|
85
provisioning/script.py
Normal file
85
provisioning/script.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#!/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()
|
||||||
|
|
11
provisioning/shell.nix
Normal file
11
provisioning/shell.nix
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{ 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
shell.nix
Symbolic link
1
shell.nix
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
provisioning/shell.nix
|
Loading…
Reference in a new issue