feat: Introduce AI code hinting, enforce single-tenant organization model, and add a Code Lab block component.
This commit is contained in:
@@ -211,18 +211,12 @@ CREATE OR REPLACE FUNCTION fn_register_user(
|
||||
p_password_hash VARCHAR(255),
|
||||
p_full_name VARCHAR(255),
|
||||
p_role VARCHAR(50),
|
||||
p_org_name VARCHAR(255)
|
||||
p_org_name VARCHAR(255) -- Preserved for signature compatibility but ignored
|
||||
) RETURNS SETOF users AS $$
|
||||
DECLARE
|
||||
v_org_id UUID;
|
||||
v_org_id UUID := '00000000-0000-0000-0000-000000000001';
|
||||
BEGIN
|
||||
-- Find or create organization
|
||||
INSERT INTO organizations (name)
|
||||
VALUES (p_org_name)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id INTO v_org_id;
|
||||
|
||||
-- Create user
|
||||
-- Create user in default organization
|
||||
RETURN QUERY
|
||||
INSERT INTO users (email, password_hash, full_name, role, organization_id)
|
||||
VALUES (p_email, p_password_hash, p_full_name, p_role, v_org_id)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Migration: Enforce single-tenant and update organization name
|
||||
-- This migration updates fn_register_user to always use the default organization ID (0...1)
|
||||
-- but allows updating its name during the initial registration.
|
||||
|
||||
CREATE OR REPLACE FUNCTION fn_register_user(
|
||||
p_email VARCHAR(255),
|
||||
p_password_hash VARCHAR(255),
|
||||
p_full_name VARCHAR(255),
|
||||
p_role VARCHAR(50),
|
||||
p_org_name VARCHAR(255) DEFAULT NULL
|
||||
) RETURNS SETOF users AS $$
|
||||
DECLARE
|
||||
v_org_id UUID := '00000000-0000-0000-0000-000000000001';
|
||||
BEGIN
|
||||
-- Update the default organization name if a custom name is provided
|
||||
IF p_org_name IS NOT NULL AND p_org_name <> '' AND p_org_name <> 'Default Organization' AND p_org_name <> 'Organización por Defecto' THEN
|
||||
UPDATE organizations
|
||||
SET name = p_org_name,
|
||||
updated_at = NOW()
|
||||
WHERE id = v_org_id;
|
||||
END IF;
|
||||
|
||||
-- Create user and assign to the default organization
|
||||
RETURN QUERY
|
||||
INSERT INTO users (email, password_hash, full_name, role, organization_id)
|
||||
VALUES (p_email, p_password_hash, p_full_name, p_role, v_org_id)
|
||||
RETURNING *;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -8,8 +8,9 @@ use axum::{
|
||||
};
|
||||
use bcrypt::{DEFAULT_COST, hash, verify};
|
||||
use chrono::{DateTime, Utc};
|
||||
use common::auth::{Claims, create_jwt, create_preview_token};
|
||||
use common::middleware::Org;
|
||||
pub use common::auth::Claims;
|
||||
pub use common::middleware::Org;
|
||||
use common::auth::{create_jwt, create_preview_token};
|
||||
use common::models::{
|
||||
AuthResponse, Course, CourseAnalytics, Lesson, Module, Organization, PublishedCourse,
|
||||
PublishedModule, User, UserResponse, CourseInstructor,
|
||||
@@ -1831,6 +1832,122 @@ pub async fn generate_mermaid_diagram(
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GenerateCodeLabPayload {
|
||||
pub language: Option<String>,
|
||||
pub prompt_hint: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn generate_code_lab(
|
||||
Org(org_ctx): Org,
|
||||
_claims: Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
Json(payload): Json<GenerateCodeLabPayload>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
tracing::info!("Generating Code Lab for lesson_id={}", lesson_id);
|
||||
|
||||
let lesson = sqlx::query_as::<_, Lesson>("SELECT * FROM lessons WHERE id = $1 AND organization_id = $2")
|
||||
.bind(lesson_id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::NOT_FOUND, "Lección no encontrada".into()))?;
|
||||
|
||||
let provider = env::var("AI_PROVIDER").unwrap_or_else(|_| "local".to_string());
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let (url, auth_header, model) = if provider == "local" {
|
||||
let base_url = env::var("LOCAL_OLLAMA_URL").unwrap_or_else(|_| "http://ollama:11434".to_string());
|
||||
let model = env::var("LOCAL_LLM_MODEL").unwrap_or_else(|_| "llama3.2:3b".to_string());
|
||||
(format!("{}/v1/chat/completions", base_url), "".to_string(), model)
|
||||
} else {
|
||||
(
|
||||
"https://api.openai.com/v1/chat/completions".to_string(),
|
||||
format!("Bearer {}", env::var("OPENAI_API_KEY").unwrap_or_default()),
|
||||
"gpt-4o".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let transcription_str = lesson.transcription.as_ref().and_then(|v| v.as_str());
|
||||
let summary_str = lesson.summary.as_deref();
|
||||
let lesson_context = transcription_str.or(summary_str).unwrap_or("Conceptos generales de la lección.");
|
||||
let language = payload.language.unwrap_or_else(|| "python".to_string());
|
||||
let user_hint = payload.prompt_hint.unwrap_or_else(|| "Diseña un ejercicio práctico que refuerce los conceptos de la lección.".to_string());
|
||||
|
||||
let system_prompt = format!(
|
||||
"Eres un experto pedagogo y programador especializado en crear ejercicios de código educativos.\n\
|
||||
Tu tarea es generar un ejercicio de programación en {} basado en el siguiente contenido de la lección.\n\
|
||||
INSTRUCCIONES CRÍTICAS:\n\
|
||||
1. Responde ÚNICAMENTE con un objeto JSON válido. Sin texto adicional, sin explicaciones, sin bloques markdown.\n\
|
||||
2. El JSON debe tener exactamente esta estructura:\n\
|
||||
{{\"title\": \"string\", \"instructions\": \"string\", \"initial_code\": \"string\", \"solution\": \"string\", \"test_cases\": [{{\"description\": \"string\", \"expected\": \"string\"}}]}}\n\
|
||||
3. El campo 'initial_code' debe ser un esqueleto con comentarios TODO para que el estudiante lo complete.\n\
|
||||
4. El campo 'solution' debe ser la solución completa.\n\
|
||||
5. El campo 'test_cases' debe contener 2-3 casos de prueba descriptivos.\n\
|
||||
6. Las instrucciones deben ser claras y pedagógicamente apropiadas.\n\n\
|
||||
Contexto de la lección:\n{}\n\n\
|
||||
Instrucciones adicionales:\n{}",
|
||||
language, lesson_context, user_hint
|
||||
);
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"model": model,
|
||||
"messages": [
|
||||
{ "role": "system", "content": system_prompt },
|
||||
{ "role": "user", "content": "Genera el ejercicio de código ahora." }
|
||||
],
|
||||
"temperature": 0.4
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("LLM Request failed: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Error contacting AI provider".into())
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let err_body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("LLM Error response: {}", err_body);
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, "AI provider returned an error".into()));
|
||||
}
|
||||
|
||||
let ai_data: serde_json::Value = response.json().await.map_err(|e| {
|
||||
tracing::error!("Failed to parse LLM JSON: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Error parsing AI response".into())
|
||||
})?;
|
||||
|
||||
let ai_text = ai_data["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or("{}")
|
||||
.trim();
|
||||
|
||||
// Strip potential markdown code fences
|
||||
let cleaned = ai_text
|
||||
.strip_prefix("```json\n").unwrap_or(ai_text)
|
||||
.strip_prefix("```\n").unwrap_or(ai_text)
|
||||
.strip_suffix("```").unwrap_or(ai_text)
|
||||
.trim();
|
||||
|
||||
let exercise: serde_json::Value = serde_json::from_str(cleaned).map_err(|e| {
|
||||
tracing::error!("Failed to parse exercise JSON from LLM: {} | raw: {}", e, cleaned);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "AI returned invalid exercise JSON".into())
|
||||
})?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"language": language,
|
||||
"title": exercise["title"],
|
||||
"instructions": exercise["instructions"],
|
||||
"initial_code": exercise["initial_code"],
|
||||
"solution": exercise["solution"],
|
||||
"test_cases": exercise["test_cases"],
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GenerateHotspotsPayload {
|
||||
pub image_url: String,
|
||||
@@ -2082,15 +2199,6 @@ pub struct AuthPayload {
|
||||
pub organization_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ProvisionPayload {
|
||||
pub org_name: String,
|
||||
pub org_domain: Option<String>,
|
||||
pub admin_email: String,
|
||||
pub admin_password: String,
|
||||
pub admin_full_name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct AdminCreateUserPayload {
|
||||
pub email: String,
|
||||
@@ -3130,176 +3238,9 @@ pub async fn update_user(
|
||||
}))
|
||||
}
|
||||
|
||||
// Organizations Management (Plural/Admin)
|
||||
pub async fn get_organizations(
|
||||
claims: common::auth::Claims,
|
||||
State(pool): State<PgPool>,
|
||||
) -> Result<Json<Vec<Organization>>, StatusCode> {
|
||||
if claims.role != "admin" {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
let orgs =
|
||||
sqlx::query_as::<_, Organization>("SELECT * FROM organizations ORDER BY created_at DESC")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to fetch organizations: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(orgs))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OrgSearchQuery {
|
||||
pub q: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
pub struct OrgSearchResult {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub domain: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn search_organizations(
|
||||
State(pool): State<PgPool>,
|
||||
Query(query): Query<OrgSearchQuery>,
|
||||
) -> Result<Json<Vec<OrgSearchResult>>, StatusCode> {
|
||||
if query.q.trim().is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
}
|
||||
|
||||
let search_term = format!("%{}%", query.q.trim());
|
||||
|
||||
let orgs = sqlx::query_as::<_, OrgSearchResult>(
|
||||
"SELECT id, name, domain FROM organizations WHERE name ILIKE $1 OR domain ILIKE $1 ORDER BY name ASC LIMIT 10"
|
||||
)
|
||||
.bind(search_term)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to search organizations: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(orgs))
|
||||
}
|
||||
|
||||
pub async fn create_organization(
|
||||
claims: common::auth::Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<Json<Organization>, StatusCode> {
|
||||
if claims.role != "admin" {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
let name = payload
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or(StatusCode::BAD_REQUEST)?;
|
||||
let domain = payload.get("domain").and_then(|v| v.as_str());
|
||||
|
||||
let org = sqlx::query_as::<_, Organization>(
|
||||
"INSERT INTO organizations (name, domain) VALUES ($1, $2) RETURNING *",
|
||||
)
|
||||
.bind(name)
|
||||
.bind(domain)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create organization: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
Ok(Json(org))
|
||||
}
|
||||
|
||||
pub async fn update_organization(
|
||||
claims: common::auth::Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<Json<Organization>, (StatusCode, String)> {
|
||||
// Only super admins or admins of the same org?
|
||||
// Usually editing other orgs is a Super Admin only task.
|
||||
let is_super_admin = claims.role == "admin"
|
||||
&& claims.org == Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
|
||||
if !is_super_admin {
|
||||
return Err((StatusCode::FORBIDDEN, "Super Admin access required".into()));
|
||||
}
|
||||
|
||||
let name = payload.get("name").and_then(|v| v.as_str());
|
||||
let domain = payload.get("domain").and_then(|v| v.as_str());
|
||||
|
||||
let org = sqlx::query_as::<_, Organization>(
|
||||
"UPDATE organizations SET
|
||||
name = COALESCE($1, name),
|
||||
domain = COALESCE($2, domain),
|
||||
updated_at = NOW()
|
||||
WHERE id = $3 RETURNING *"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(domain)
|
||||
.bind(id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to update organization: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to update organization".into())
|
||||
})?;
|
||||
|
||||
Ok(Json(org))
|
||||
}
|
||||
|
||||
pub async fn provision_organization(
|
||||
claims: common::auth::Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Json(payload): Json<ProvisionPayload>,
|
||||
) -> Result<Json<Organization>, (StatusCode, String)> {
|
||||
if claims.role != "admin" || claims.org != Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap() {
|
||||
return Err((StatusCode::FORBIDDEN, "Super Admin access required".into()));
|
||||
}
|
||||
|
||||
let mut tx = pool.begin().await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let org = sqlx::query_as::<_, Organization>(
|
||||
"INSERT INTO organizations (name, domain) VALUES ($1, $2) RETURNING *"
|
||||
)
|
||||
.bind(&payload.org_name)
|
||||
.bind(&payload.org_domain)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create organization: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to create organization".into())
|
||||
})?;
|
||||
|
||||
let password_hash = hash(payload.admin_password, DEFAULT_COST)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Hashing failed".into()))?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO users (email, password_hash, full_name, role, organization_id) VALUES ($1, $2, $3, $4, $5)"
|
||||
)
|
||||
.bind(&payload.admin_email)
|
||||
.bind(password_hash)
|
||||
.bind(&payload.admin_full_name)
|
||||
.bind("admin")
|
||||
.bind(org.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create admin user: {}", e);
|
||||
(StatusCode::CONFLICT, "User already exists or DB error".into())
|
||||
})?;
|
||||
|
||||
tx.commit().await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(org))
|
||||
}
|
||||
// Organizations Management (Simplified for Single-Tenant)
|
||||
// Multi-tenant organization management has been removed.
|
||||
// The system now operates on a single default organization.
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateWebhookPayload {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
};
|
||||
use common::models::Organization;
|
||||
@@ -11,7 +11,7 @@ use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::handlers::log_action;
|
||||
use super::handlers::{log_action, Org};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct BrandingPayload {
|
||||
@@ -35,8 +35,8 @@ pub struct BrandingResponse {
|
||||
// Upload organization logo
|
||||
pub async fn upload_organization_logo(
|
||||
claims: common::auth::Claims,
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
Path(org_id): Path<Uuid>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// Only admins can upload logos
|
||||
@@ -46,7 +46,7 @@ pub async fn upload_organization_logo(
|
||||
|
||||
// Verify organization exists and user has access
|
||||
let _ = sqlx::query_as::<_, Organization>("SELECT * FROM organizations WHERE id = $1")
|
||||
.bind(org_id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::NOT_FOUND, "Organization not found".into()))?;
|
||||
@@ -98,7 +98,7 @@ pub async fn upload_organization_logo(
|
||||
})?;
|
||||
|
||||
// Generate unique filename
|
||||
let unique_filename = format!("{}_{}.{}", org_id, uuid::Uuid::new_v4(), ext);
|
||||
let unique_filename = format!("{}_{}.{}", org_ctx.id, uuid::Uuid::new_v4(), ext);
|
||||
let filepath = format!("uploads/org-logos/{}", unique_filename);
|
||||
|
||||
// Save file
|
||||
@@ -113,7 +113,7 @@ pub async fn upload_organization_logo(
|
||||
let logo_url = format!("/{}", filepath);
|
||||
sqlx::query("UPDATE organizations SET logo_url = $1 WHERE id = $2")
|
||||
.bind(&logo_url)
|
||||
.bind(org_id)
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -129,7 +129,7 @@ pub async fn upload_organization_logo(
|
||||
claims.sub,
|
||||
"UPDATE_LOGO",
|
||||
"Organization",
|
||||
org_id,
|
||||
org_ctx.id,
|
||||
json!({"logo_url": &logo_url}),
|
||||
)
|
||||
.await;
|
||||
@@ -147,8 +147,8 @@ pub async fn upload_organization_logo(
|
||||
// Upload organization favicon
|
||||
pub async fn upload_organization_favicon(
|
||||
claims: common::auth::Claims,
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
Path(org_id): Path<Uuid>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// Only admins can upload favicons
|
||||
@@ -158,7 +158,7 @@ pub async fn upload_organization_favicon(
|
||||
|
||||
// Verify organization exists and user has access
|
||||
let _ = sqlx::query_as::<_, Organization>("SELECT * FROM organizations WHERE id = $1")
|
||||
.bind(org_id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::NOT_FOUND, "Organization not found".into()))?;
|
||||
@@ -210,7 +210,7 @@ pub async fn upload_organization_favicon(
|
||||
})?;
|
||||
|
||||
// Generate unique filename
|
||||
let unique_filename = format!("{}_{}.{}", org_id, uuid::Uuid::new_v4(), ext);
|
||||
let unique_filename = format!("{}_{}.{}", org_ctx.id, uuid::Uuid::new_v4(), ext);
|
||||
let filepath = format!("uploads/org-favicons/{}", unique_filename);
|
||||
|
||||
// Save file
|
||||
@@ -225,7 +225,7 @@ pub async fn upload_organization_favicon(
|
||||
let favicon_url = format!("/{}", filepath);
|
||||
sqlx::query("UPDATE organizations SET favicon_url = $1 WHERE id = $2")
|
||||
.bind(&favicon_url)
|
||||
.bind(org_id)
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -241,7 +241,7 @@ pub async fn upload_organization_favicon(
|
||||
claims.sub,
|
||||
"UPDATE_FAVICON",
|
||||
"Organization",
|
||||
org_id,
|
||||
org_ctx.id,
|
||||
json!({"favicon_url": &favicon_url}),
|
||||
)
|
||||
.await;
|
||||
@@ -259,8 +259,8 @@ pub async fn upload_organization_favicon(
|
||||
// Update organization branding colors
|
||||
pub async fn update_organization_branding(
|
||||
claims: common::auth::Claims,
|
||||
Org(org_ctx): Org,
|
||||
State(pool): State<PgPool>,
|
||||
Path(org_id): Path<Uuid>,
|
||||
Json(payload): Json<BrandingPayload>,
|
||||
) -> Result<Json<Organization>, (StatusCode, String)> {
|
||||
// Only admins can update branding
|
||||
@@ -310,7 +310,7 @@ pub async fn update_organization_branding(
|
||||
.bind(&payload.secondary_color)
|
||||
.bind(&payload.platform_name)
|
||||
.bind(&payload.logo_variant)
|
||||
.bind(org_id)
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -326,7 +326,7 @@ pub async fn update_organization_branding(
|
||||
claims.sub,
|
||||
"UPDATE_BRANDING",
|
||||
"Organization",
|
||||
org_id,
|
||||
org_ctx.id,
|
||||
json!(payload),
|
||||
)
|
||||
.await;
|
||||
@@ -337,10 +337,9 @@ pub async fn update_organization_branding(
|
||||
// Get organization branding (public endpoint)
|
||||
pub async fn get_organization_branding(
|
||||
State(pool): State<PgPool>,
|
||||
Path(org_id): Path<Uuid>,
|
||||
) -> Result<Json<BrandingResponse>, StatusCode> {
|
||||
let org = sqlx::query_as::<_, Organization>("SELECT * FROM organizations WHERE id = $1")
|
||||
.bind(org_id)
|
||||
.bind(Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
|
||||
@@ -150,6 +150,7 @@ async fn main() {
|
||||
.route("/lessons/{id}/generate-role-play", post(handlers::generate_role_play))
|
||||
.route("/lessons/{id}/generate-hotspots", post(handlers::generate_hotspots))
|
||||
.route("/lessons/{id}/generate-mermaid", post(handlers::generate_mermaid_diagram))
|
||||
.route("/lessons/{id}/generate-code-lab", post(handlers::generate_code_lab))
|
||||
.route("/courses/generate", post(handlers::generate_course))
|
||||
.route("/courses/{id}/export", get(handlers::export_course))
|
||||
.route("/courses/import", post(handlers::import_course))
|
||||
@@ -172,12 +173,14 @@ async fn main() {
|
||||
.route("/api/assets/upload", post(handlers_assets::upload_asset))
|
||||
.route("/api/assets/{id}", delete(handlers_assets::delete_asset))
|
||||
.layer(DefaultBodyLimit::disable())
|
||||
/*
|
||||
.route(
|
||||
"/organizations",
|
||||
get(handlers::get_organizations).post(handlers::create_organization),
|
||||
)
|
||||
.route("/organizations/{id}", put(handlers::update_organization))
|
||||
.route("/admin/provision", post(handlers::provision_organization))
|
||||
*/
|
||||
.route(
|
||||
"/webhooks",
|
||||
get(handlers::get_webhooks).post(handlers::create_webhook),
|
||||
@@ -192,15 +195,15 @@ async fn main() {
|
||||
get(handlers::get_sso_config).put(handlers::update_sso_config),
|
||||
)
|
||||
.route(
|
||||
"/organizations/{id}/logo",
|
||||
"/organization/logo",
|
||||
post(handlers_branding::upload_organization_logo),
|
||||
)
|
||||
.route(
|
||||
"/organizations/{id}/favicon",
|
||||
"/organization/favicon",
|
||||
post(handlers_branding::upload_organization_favicon),
|
||||
)
|
||||
.route(
|
||||
"/organizations/{id}/branding",
|
||||
"/organization/branding",
|
||||
axum::routing::put(handlers_branding::update_organization_branding),
|
||||
)
|
||||
// Content Libraries routes
|
||||
@@ -290,11 +293,7 @@ async fn main() {
|
||||
.route("/auth/sso/login/{org_id}", get(handlers::sso_login_init))
|
||||
.route("/auth/sso/callback", get(handlers::sso_callback))
|
||||
.route(
|
||||
"/organizations/search",
|
||||
get(handlers::search_organizations),
|
||||
)
|
||||
.route(
|
||||
"/organizations/{id}/branding",
|
||||
"/branding",
|
||||
get(handlers_branding::get_organization_branding),
|
||||
)
|
||||
.nest_service("/assets", tower_http::services::ServeDir::new("uploads"))
|
||||
|
||||
Reference in New Issue
Block a user