Hi sir!!
This commit is contained in:
parent
2663d1f5fc
commit
ba933c7903
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
||||
# .env
|
||||
DATABASE_URL=mysql://lms_user:lms_password@db:3306/lms_db
|
||||
SERVER_ADDR=0.0.0.0:8080
|
||||
# SECRET_KEY=your_very_secret_jwt_key_here # Needed if using JWT later
|
||||
RUST_LOG=actix_web=info,lms_backend=info # Logging level
|
||||
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "lms-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
actix-cors = "0.7" # For enabling Cross-Origin Resource Sharing
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
dotenv = "0.15" # For loading .env files
|
||||
sqlx = { version = "0.7", features = ["runtime-actix-native-tls", "mysql", "chrono", "macros"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
# srp = "0.6" # For PAKE SRP (NOTE: Need careful state management for handshake)
|
||||
# We'll outline the SRP flow, but full implementation requires managing server state (b value) between requests.
|
||||
# Using simple password hashing for now as a placeholder until full SRP state management is added.
|
||||
argon2 = "0.5"
|
||||
rand = "0.8" # Needed for salt generation with Argon2
|
||||
thiserror = "1.0" # For cleaner error handling
|
||||
env_logger = "0.11" # For logging
|
||||
futures = "0.3" # Often needed for async operations
|
||||
uuid = { version = "1", features = ["v4", "serde"] } # For potential session tokens
|
||||
hex = "0.4" # For handling hex representations (e.g., for SRP values if used)
|
||||
|
||||
# PAKE SRP dependency (choose one, `srp` is common)
|
||||
srp = "0.6"
|
||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@ -0,0 +1,54 @@
|
||||
# Dockerfile
|
||||
|
||||
# ---- Builder Stage ----
|
||||
FROM rust:1.78-slim AS builder
|
||||
|
||||
# Install build dependencies (like openssl for native-tls feature of sqlx)
|
||||
# Use Debian package names
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy manifests first to leverage Docker cache for dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
# Dummy src/main.rs to build dependencies only if needed
|
||||
RUN mkdir src && echo "fn main(){}" > src/main.rs
|
||||
# Build dependencies only (can take time)
|
||||
# Use --release for optimized dependencies if desired, but target/debug might be faster for iteration
|
||||
# RUN cargo build --release
|
||||
RUN cargo build
|
||||
|
||||
# Copy the actual source code
|
||||
COPY src ./src
|
||||
|
||||
# Build the final application binary (optimized for release)
|
||||
# Ensure previous build steps are cleaned if needed
|
||||
RUN touch src/main.rs && cargo build --release
|
||||
|
||||
|
||||
# ---- Runtime Stage ----
|
||||
FROM debian:stable-slim AS runtime
|
||||
|
||||
# Install runtime dependencies (like ca-certificates and openssl libs)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the compiled binary from the builder stage
|
||||
COPY --from=builder /app/target/release/lms-backend /usr/local/bin/lms-backend
|
||||
|
||||
# Copy .env file - Alternatively, manage via Docker Compose environment vars
|
||||
# COPY .env .env # If you want .env inside the image (less flexible)
|
||||
|
||||
# Expose the port the application listens on (from .env or default)
|
||||
EXPOSE 8080
|
||||
|
||||
# Set the entrypoint to run the application
|
||||
# Use environment variables for configuration (DATABASE_URL, SERVER_ADDR) passed via Docker Compose
|
||||
CMD ["lms-backend"]
|
||||
59
docker-compose.yml
Normal file
59
docker-compose.yml
Normal file
@ -0,0 +1,59 @@
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.11 # Use a specific stable version
|
||||
container_name: lms_mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-supersecretroot} # Use default if not set in host .env
|
||||
MARIADB_DATABASE: ${MARIADB_DATABASE:-lms_db}
|
||||
MARIADB_USER: ${MARIADB_USER:-lms_user}
|
||||
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-lms_password}
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql # Persist data
|
||||
ports:
|
||||
# Only expose if you need direct access from host machine, otherwise backend connects via internal network
|
||||
# - "3306:3306"
|
||||
- "13306:3306" # Expose on 13306 to avoid host conflicts
|
||||
networks:
|
||||
- lms_network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${MARIADB_USER:-lms_user}", "-p${MARIADB_PASSWORD:-lms_password}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
container_name: lms_backend
|
||||
build:
|
||||
context: ./lms-backend # Path to the directory containing the Dockerfile
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # Wait for DB to be healthy
|
||||
environment:
|
||||
# Pass necessary environment variables to the backend container
|
||||
# Ensure these match the .env file used by the Rust app OR override them here
|
||||
DATABASE_URL: mysql://${MARIADB_USER:-lms_user}:${MARIADB_PASSWORD:-lms_password}@db:3306/${MARIADB_DATABASE:-lms_db}
|
||||
SERVER_ADDR: 0.0.0.0:8080
|
||||
RUST_LOG: ${RUST_LOG:-actix_web=info,lms_backend=info}
|
||||
# SECRET_KEY: ${SECRET_KEY:-your_fallback_secret} # If using JWT
|
||||
ports:
|
||||
- "8080:8080" # Map container port 8080 to host port 8080
|
||||
networks:
|
||||
- lms_network
|
||||
# Optional: Mount local code for development (use with caution, requires rebuilds/restarts)
|
||||
# volumes:
|
||||
# - ./lms-backend/src:/app/src
|
||||
# - ./lms-backend/Cargo.toml:/app/Cargo.toml
|
||||
# - ./lms-backend/Cargo.lock:/app/Cargo.lock
|
||||
|
||||
volumes:
|
||||
mariadb_data: # Define the named volume for persistence
|
||||
|
||||
networks:
|
||||
lms_network: # Define the network for services communication
|
||||
driver: bridge
|
||||
21
src/config.rs
Normal file
21
src/config.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use std::env;
|
||||
|
||||
#[derive(Clone)] // Clone needed for potential app_data sharing
|
||||
pub struct Config {
|
||||
pub database_url: String,
|
||||
pub server_addr: String,
|
||||
// pub secret_key: String, // Add if using JWT
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Result {
|
||||
let database_url = env::var("DATABASE_URL")?;
|
||||
let server_addr = env::var("SERVER_ADDR")?;
|
||||
// let secret_key = env::var("SECRET_KEY")?; // Add if using JWT
|
||||
Ok(Config {
|
||||
database_url,
|
||||
server_addr,
|
||||
// secret_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
217
src/db.rs
Normal file
217
src/db.rs
Normal file
@ -0,0 +1,217 @@
|
||||
use crate::errors::AppError;
|
||||
use crate::models::{Course, Student, Teacher};
|
||||
use sqlx::{MySql, Pool};
|
||||
|
||||
// Function to create tables if they don't exist
|
||||
pub async fn init_db(pool: &Pool) -> Result<(), AppError> {
|
||||
log::info!("Creating tables if they do not exist...");
|
||||
|
||||
// Use UNSIGNED for IDs if they are always non-negative and you might need larger ranges
|
||||
// Use TEXT or appropriate VARCHAR length for SRP salt/verifier (hex encoded can be long)
|
||||
sqlx::query!(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS students (
|
||||
user_id INT PRIMARY KEY,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
birthday DATE NULL,
|
||||
srp_salt TEXT NULL,
|
||||
srp_verifier TEXT NULL,
|
||||
school_id VARCHAR(50) NULL, -- Added based on frontend expectations
|
||||
profile_picture VARCHAR(255) NULL -- Added based on frontend expectations
|
||||
)
|
||||
"#
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS teachers (
|
||||
user_id INT PRIMARY KEY,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
birthday DATE NULL,
|
||||
srp_salt TEXT NULL,
|
||||
srp_verifier TEXT NULL,
|
||||
profile_picture VARCHAR(255) NULL -- Added based on frontend expectations
|
||||
)
|
||||
"#
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS courses (
|
||||
course_code INT PRIMARY KEY, -- As per spec
|
||||
course_name VARCHAR(255) NOT NULL
|
||||
)
|
||||
"#
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS enrollments (
|
||||
student_id INT NOT NULL,
|
||||
course_id INT NOT NULL,
|
||||
enrollment_date DATE NULL,
|
||||
PRIMARY KEY (student_id, course_id),
|
||||
FOREIGN KEY (student_id) REFERENCES students(user_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(course_code) ON DELETE CASCADE
|
||||
)
|
||||
"#
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
log::info!("Tables checked/created successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function to populate some initial data (e.g., courses) - MAKE THIS IDEMPOTENT
|
||||
pub async fn populate_initial_data(pool: &Pool) -> Result<(), AppError> {
|
||||
log::info!("Attempting to populate initial data (Courses)...");
|
||||
|
||||
// Check if courses already exist to avoid errors on restart
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM courses")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if count.0 == 0 {
|
||||
log::info!("No existing courses found, adding sample data...");
|
||||
let courses = vec![
|
||||
(101, "Introduction to Programming"),
|
||||
(102, "Data Structures"),
|
||||
(201, "Calculus I"),
|
||||
(205, "Linear Algebra"),
|
||||
];
|
||||
|
||||
for (code, name) in courses {
|
||||
sqlx::query!(
|
||||
"INSERT INTO courses (course_code, course_name) VALUES (?, ?)",
|
||||
code,
|
||||
name
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
log::info!("Sample courses added.");
|
||||
|
||||
// Add a sample student and teacher WITHOUT passwords/SRP data
|
||||
// User ID 123456 is used in frontend mock login
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO students (user_id, first_name, last_name, school_id, birthday)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE user_id=user_id -- Do nothing if exists
|
||||
"#,
|
||||
123456, "John", "Doe", "S123456", chrono::NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||
).execute(pool).await?;
|
||||
log::info!("Sample student added (ID: 123456).");
|
||||
|
||||
// Add a sample teacher (maybe overlapping ID to test login logic)
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO teachers (user_id, first_name, last_name, birthday)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE user_id=user_id -- Do nothing if exists
|
||||
"#,
|
||||
5001, "Alice", "Smith", chrono::NaiveDate::from_ymd_opt(1985, 5, 15)
|
||||
).execute(pool).await?;
|
||||
log::info!("Sample teacher added (ID: 5001).");
|
||||
|
||||
// Example of overlapping ID (same as student)
|
||||
// This teacher won't be able to log in until password/SRP is set,
|
||||
// and login logic must handle potential ID collision if SRP data exists for both.
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO teachers (user_id, first_name, last_name, birthday)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE user_id=user_id -- Do nothing if exists
|
||||
"#,
|
||||
123456, "Jane", "Teacher", chrono::NaiveDate::from_ymd_opt(1990, 3, 10)
|
||||
).execute(pool).await?;
|
||||
log::info!("Sample teacher added with overlapping ID (ID: 123456).");
|
||||
|
||||
|
||||
} else {
|
||||
log::info!("Courses table already populated.");
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Example Fetch Functions ---
|
||||
|
||||
pub async fn find_student_by_id(pool: &Pool, user_id: i32) -> Result, AppError> {
|
||||
let student = sqlx::query_as!(
|
||||
Student,
|
||||
r#"
|
||||
SELECT user_id, first_name, last_name, birthday, srp_salt, srp_verifier, school_id, profile_picture
|
||||
FROM students
|
||||
WHERE user_id = ?
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(student)
|
||||
}
|
||||
|
||||
pub async fn find_teacher_by_id(pool: &Pool, user_id: i32) -> Result, AppError> {
|
||||
let teacher = sqlx::query_as!(
|
||||
Teacher,
|
||||
r#"
|
||||
SELECT user_id, first_name, last_name, birthday, srp_salt, srp_verifier, profile_picture
|
||||
FROM teachers
|
||||
WHERE user_id = ?
|
||||
"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(teacher)
|
||||
}
|
||||
|
||||
// --- Count Functions ---
|
||||
pub async fn count_students(pool: &Pool) -> Result {
|
||||
let result = sqlx::query!("SELECT COUNT(*) as count FROM students")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(result.count)
|
||||
}
|
||||
|
||||
pub async fn count_teachers(pool: &Pool) -> Result {
|
||||
let result = sqlx::query!("SELECT COUNT(*) as count FROM teachers")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
Ok(result.count)
|
||||
}
|
||||
|
||||
// --- Update Functions ---
|
||||
// Add functions to update SRP salt/verifier when a user sets/changes their password
|
||||
pub async fn update_student_srp(pool: &Pool, user_id: i32, salt: &str, verifier: &str) -> Result<(), AppError> {
|
||||
sqlx::query!(
|
||||
"UPDATE students SET srp_salt = ?, srp_verifier = ? WHERE user_id = ?",
|
||||
salt, verifier, user_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_teacher_srp(pool: &Pool, user_id: i32, salt: &str, verifier: &str) -> Result<(), AppError> {
|
||||
sqlx::query!(
|
||||
"UPDATE teachers SET srp_salt = ?, srp_verifier = ? WHERE user_id = ?",
|
||||
salt, verifier, user_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Add functions for get_student_list (with joins/filtering), manage students, enrollments etc.
|
||||
81
src/errors.rs
Normal file
81
src/errors.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use serde::Serialize;
|
||||
use sqlx::Error as SqlxError;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(#[from] SqlxError),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
ConfigError(#[from] std::env::VarError),
|
||||
|
||||
#[error("Not Found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Authentication Failed: {0}")]
|
||||
AuthenticationError(String),
|
||||
|
||||
#[error("Invalid Input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Authorization Failed: {0}")]
|
||||
AuthorizationError(String),
|
||||
|
||||
#[error("Internal Server Error")]
|
||||
InternalError(String), // Keep internal details out of response
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
success: bool,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl ResponseError for AppError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
log::error!("Error handled: {:?}", self); // Log the full error
|
||||
|
||||
let (status_code, message) = match self {
|
||||
AppError::DatabaseError(_) => (
|
||||
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Database error occurred.".to_string(),
|
||||
),
|
||||
AppError::ConfigError(_) => (
|
||||
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Configuration error.".to_string(),
|
||||
),
|
||||
AppError::NotFound(ref msg) => (actix_web::http::StatusCode::NOT_FOUND, msg.clone()),
|
||||
AppError::AuthenticationError(ref msg) => {
|
||||
(actix_web::http::StatusCode::UNAUTHORIZED, msg.clone())
|
||||
}
|
||||
AppError::InvalidInput(ref msg) => {
|
||||
(actix_web::http::StatusCode::BAD_REQUEST, msg.clone())
|
||||
}
|
||||
AppError::AuthorizationError(ref msg) => {
|
||||
(actix_web::http::StatusCode::FORBIDDEN, msg.clone())
|
||||
}
|
||||
AppError::InternalError(_) => (
|
||||
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"An internal error occurred.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
HttpResponse::build(status_code).json(ErrorResponse {
|
||||
success: false,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for converting Option to AppError::NotFound
|
||||
pub trait OptionExt {
|
||||
fn ok_or_not_found(self, message: &str) -> Result;
|
||||
}
|
||||
|
||||
impl OptionExt for Option {
|
||||
fn ok_or_not_found(self, message: &str) -> Result {
|
||||
self.ok_or_else(|| AppError::NotFound(message.to_string()))
|
||||
}
|
||||
}
|
||||
496
src/handlers.rs
Normal file
496
src/handlers.rs
Normal file
@ -0,0 +1,496 @@
|
||||
use actix_web::{web, get, post, put, HttpResponse, Responder, Scope};
|
||||
use sqlx::{MySql, Pool};
|
||||
use crate::{
|
||||
db,
|
||||
errors::{AppError, OptionExt},
|
||||
models::*,
|
||||
};
|
||||
use srp::server::{SrpServer, SrpServerChallenge}; // Using the `srp` crate
|
||||
use srp::{types::*, EphemeralKey, PublicEphemeralKey}; // Import necessary SRP types
|
||||
use hex; // For encoding/decoding SRP values
|
||||
use std::collections::HashMap; // Example for temporary state store
|
||||
use std::sync::Mutex; // For thread-safe access to state store
|
||||
use uuid::Uuid;
|
||||
|
||||
// --- Temporary State Store for SRP Handshake ---
|
||||
// WARNING: This in-memory store is NOT suitable for production.
|
||||
// It will lose state on restart and doesn't scale across multiple instances.
|
||||
// Use a proper distributed cache (like Redis) or database table for production.
|
||||
struct AppState {
|
||||
srp_challenges: Mutex, String)>>, // Key: user_id_type (e.g., "123456_student"), Value: (Challenge state, User Type)
|
||||
active_sessions: Mutex>, // Key: session_token (UUID), Value: User Data
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new() -> Self {
|
||||
AppState {
|
||||
srp_challenges: Mutex::new(HashMap::new()),
|
||||
active_sessions: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- Helper Functions ---
|
||||
|
||||
fn create_api_response(data: Option, success: bool, message: Option) -> HttpResponse {
|
||||
HttpResponse::Ok().json(ApiResponse {
|
||||
success,
|
||||
data,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to parse user_id string to i32
|
||||
fn parse_user_id(user_id_str: &str) -> Result {
|
||||
user_id_str.parse::().map_err(|_| AppError::InvalidInput(format!("Invalid user ID format: {}", user_id_str)))
|
||||
}
|
||||
|
||||
|
||||
// --- Route Handlers ---
|
||||
|
||||
#[get("/health")]
|
||||
async fn health_check() -> impl Responder {
|
||||
HttpResponse::Ok().json(ApiResponse {
|
||||
success: true,
|
||||
data: Some("Healthy"),
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
|
||||
// --- SRP Login Step 1: Start ---
|
||||
#[post("/auth/srp/start")]
|
||||
async fn srp_login_start(
|
||||
pool: web::Data>,
|
||||
app_state: web::Data,
|
||||
req: web::Json,
|
||||
) -> Result {
|
||||
let user_id_str = &req.user_id;
|
||||
let user_id = parse_user_id(user_id_str)?;
|
||||
|
||||
// --- Handle potential overlapping IDs ---
|
||||
let student_data = db::find_student_by_id(&pool, user_id).await?;
|
||||
let teacher_data = db::find_teacher_by_id(&pool, user_id).await?;
|
||||
|
||||
let (salt_hex, verifier_hex, user_type) = match (student_data, teacher_data) {
|
||||
// Found only student with SRP data
|
||||
(Some(s), None) if s.srp_salt.is_some() && s.srp_verifier.is_some() => {
|
||||
log::info!("SRP Start: Found student {} with SRP data.", user_id);
|
||||
(s.srp_salt.unwrap(), s.srp_verifier.unwrap(), "student")
|
||||
},
|
||||
// Found only teacher with SRP data
|
||||
(None, Some(t)) if t.srp_salt.is_some() && t.srp_verifier.is_some() => {
|
||||
log::info!("SRP Start: Found teacher {} with SRP data.", user_id);
|
||||
(t.srp_salt.unwrap(), t.srp_verifier.unwrap(), "teacher")
|
||||
},
|
||||
// Found both, but only student has SRP data
|
||||
(Some(s), Some(_)) if s.srp_salt.is_some() && s.srp_verifier.is_some() => {
|
||||
log::warn!("SRP Start: Found student {} and teacher {} (overlapping ID), using student SRP data.", user_id, user_id);
|
||||
(s.srp_salt.unwrap(), s.srp_verifier.unwrap(), "student")
|
||||
},
|
||||
// Found both, but only teacher has SRP data (less likely scenario if student usually registers first)
|
||||
(Some(_), Some(t)) if t.srp_salt.is_some() && t.srp_verifier.is_some() => {
|
||||
log::warn!("SRP Start: Found student {} and teacher {} (overlapping ID), using teacher SRP data.", user_id, user_id);
|
||||
(t.srp_salt.unwrap(), t.srp_verifier.unwrap(), "teacher")
|
||||
},
|
||||
// Found both with SRP data - ambiguous! Prefer student? Or return error?
|
||||
(Some(s), Some(t)) if s.srp_salt.is_some() && s.srp_verifier.is_some() && t.srp_salt.is_some() && t.srp_verifier.is_some() => {
|
||||
log::error!("SRP Start: Ambiguous login for user ID {}. Both student and teacher have SRP data set.", user_id);
|
||||
// For now, let's arbitrarily choose student. A better approach might involve asking the user or having unique usernames.
|
||||
log::warn!("SRP Start: Ambiguous login for user ID {}. Defaulting to student.", user_id);
|
||||
(s.srp_salt.unwrap(), s.srp_verifier.unwrap(), "student")
|
||||
// Alternatively, return an error:
|
||||
// return Err(AppError::AuthenticationError("Ambiguous user ID. Please contact support.".to_string()));
|
||||
},
|
||||
// User exists but SRP data not set (password needs to be created)
|
||||
(Some(_), _) | (_, Some(_)) => {
|
||||
log::warn!("SRP Start: User ID {} found, but SRP salt/verifier not set. Cannot log in.", user_id);
|
||||
return Err(AppError::AuthenticationError(
|
||||
"Account exists but password not set. Cannot initiate login.".to_string(),
|
||||
));
|
||||
}
|
||||
// User not found in either table
|
||||
(None, None) => {
|
||||
log::warn!("SRP Start: User ID {} not found.", user_id);
|
||||
return Err(AppError::NotFound(format!("User ID {} not found.", user_id)));
|
||||
}
|
||||
};
|
||||
|
||||
// Decode hex salt and verifier from DB
|
||||
let salt = Salt::from_hex(&salt_hex)
|
||||
.map_err(|e| AppError::InternalError(format!("Failed to decode salt: {}", e)))?;
|
||||
let verifier = Verifier::from_hex(&verifier_hex)
|
||||
.map_err(|e| AppError::InternalError(format!("Failed to decode verifier: {}", e)))?;
|
||||
|
||||
// Generate server ephemeral keys and challenge
|
||||
// Using SHA256 as the hash function, adjust if needed (must match client)
|
||||
let server = SrpServer::::new(&verifier);
|
||||
let challenge = server.challenge(&salt).map_err(|e| {
|
||||
log::error!("SRP challenge generation error: {}", e);
|
||||
AppError::InternalError("Failed to generate SRP challenge".to_string())
|
||||
})?;
|
||||
|
||||
let server_public_ephemeral_hex = challenge.public_ephemeral().to_hex();
|
||||
|
||||
// --- Store challenge state TEMPORARILY ---
|
||||
// Associate the challenge state with the user_id and type for the verify step
|
||||
let state_key = format!("{}_{}", user_id_str, user_type);
|
||||
let mut challenges = app_state.srp_challenges.lock().unwrap(); // Handle potential poisoning in production
|
||||
challenges.insert(state_key.clone(), (challenge, user_type.to_string()));
|
||||
log::info!("Stored SRP challenge state for key: {}", state_key);
|
||||
// TODO: Add TTL for stored challenges to prevent memory leaks
|
||||
|
||||
Ok(create_api_response(
|
||||
Some(SrpLoginStartResponse {
|
||||
salt: salt_hex, // Send back the original hex salt
|
||||
server_public_ephemeral: server_public_ephemeral_hex,
|
||||
user_type: user_type.to_string(),
|
||||
}),
|
||||
true,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
// --- SRP Login Step 2: Verify ---
|
||||
#[post("/auth/srp/verify")]
|
||||
async fn srp_login_verify(
|
||||
pool: web::Data>,
|
||||
app_state: web::Data,
|
||||
req: web::Json,
|
||||
) -> Result {
|
||||
let user_id_str = &req.user_id;
|
||||
let user_type = &req.user_type; // Get user type received from client (who got it from /start)
|
||||
let state_key = format!("{}_{}", user_id_str, user_type);
|
||||
|
||||
log::info!("SRP Verify: Attempting for state key: {}", state_key);
|
||||
|
||||
// --- Retrieve challenge state ---
|
||||
let challenge_state_tuple = { // Scope the lock
|
||||
let mut challenges = app_state.srp_challenges.lock().unwrap();
|
||||
// IMPORTANT: Remove the state after retrieving it to prevent replay attacks
|
||||
challenges.remove(&state_key)
|
||||
.ok_or_else(|| {
|
||||
log::warn!("SRP Verify: No active SRP challenge found for key: {}", state_key);
|
||||
AppError::AuthenticationError("SRP session expired or invalid.".to_string())
|
||||
})?
|
||||
};
|
||||
let (challenge_state, retrieved_user_type) = challenge_state_tuple;
|
||||
if &retrieved_user_type != user_type {
|
||||
log::error!("SRP Verify: User type mismatch for key {}. Expected {}, got {}", state_key, retrieved_user_type, user_type);
|
||||
return Err(AppError::AuthenticationError("SRP session data mismatch.".to_string()));
|
||||
}
|
||||
|
||||
|
||||
// Decode client public ephemeral (A) and proof (M1)
|
||||
let client_public_ephemeral = PublicEphemeralKey::from_hex(&req.client_public_ephemeral)
|
||||
.map_err(|_| AppError::InvalidInput("Invalid client public ephemeral key format.".to_string()))?;
|
||||
let client_proof = ClientProof::from_hex(&req.client_proof)
|
||||
.map_err(|_| AppError::InvalidInput("Invalid client proof format.".to_string()))?;
|
||||
|
||||
|
||||
// Verify the client's proof using the stored challenge state
|
||||
let verification = challenge_state.verify_client(&client_public_ephemeral, &client_proof);
|
||||
|
||||
match verification {
|
||||
Ok(session) => {
|
||||
// Verification successful! Generate server proof (M2) and session token
|
||||
let server_proof_hex = session.server_proof().to_hex();
|
||||
let session_key = session.session_key(); // Use this for encrypted communication if needed
|
||||
log::info!("SRP Verify: Client proof verified for key: {}. Session key generated (first 4 bytes): {:x?}", state_key, &session_key[..4]);
|
||||
|
||||
|
||||
// --- Authentication successful, fetch user details ---
|
||||
let user_id = parse_user_id(user_id_str)?;
|
||||
let (full_name, profile_picture, school_id, birthdate) =
|
||||
match user_type.as_str() {
|
||||
"student" => {
|
||||
let student = db::find_student_by_id(&pool, user_id).await?
|
||||
.ok_or_not_found("Student data not found after successful SRP verification.")?;
|
||||
(
|
||||
format!("{} {}", student.first_name, student.last_name),
|
||||
student.profile_picture,
|
||||
student.school_id,
|
||||
student.birthday.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||
)
|
||||
}
|
||||
"teacher" => {
|
||||
let teacher = db::find_teacher_by_id(&pool, user_id).await?
|
||||
.ok_or_not_found("Teacher data not found after successful SRP verification.")?;
|
||||
(
|
||||
format!("{} {}", teacher.first_name, teacher.last_name),
|
||||
teacher.profile_picture,
|
||||
None, // Teachers might not have a school_id in this schema
|
||||
teacher.birthday.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||
)
|
||||
}
|
||||
_ => return Err(AppError::InternalError("Invalid user type encountered.".to_string())),
|
||||
};
|
||||
|
||||
|
||||
// --- Generate a session token (e.g., UUID) ---
|
||||
let session_token = Uuid::new_v4().to_string();
|
||||
|
||||
// Create login success data
|
||||
let login_data = LoginSuccessData {
|
||||
user_id: user_id_str.clone(),
|
||||
full_name,
|
||||
profile_picture,
|
||||
school_id,
|
||||
birthdate,
|
||||
token: session_token.clone(),
|
||||
user_type: user_type.clone(),
|
||||
};
|
||||
|
||||
// Store active session (replace with DB/Redis in production)
|
||||
{
|
||||
let mut sessions = app_state.active_sessions.lock().unwrap();
|
||||
sessions.insert(session_token.clone(), login_data.clone()); // Clone data for response
|
||||
log::info!("SRP Verify: Stored active session for token: {}", session_token);
|
||||
// TODO: Implement session expiry
|
||||
}
|
||||
|
||||
|
||||
// Respond with server proof and login data
|
||||
Ok(create_api_response(
|
||||
Some(SrpLoginVerifyResponse {
|
||||
server_proof: server_proof_hex,
|
||||
login_data: Some(login_data),
|
||||
}),
|
||||
true,
|
||||
None,
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
// Verification failed
|
||||
log::warn!("SRP Verify: Client proof verification failed for key {}: {}", state_key, e);
|
||||
Err(AppError::AuthenticationError("Invalid login credentials (SRP verification failed).".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add logout handler (needs to invalidate token/session)
|
||||
#[post("/auth/logout")]
|
||||
async fn logout(app_state: web::Data, /* TODO: Extract token from header */ ) -> Result {
|
||||
// 1. Extract token from Authorization header
|
||||
// let token = extract_token_from_request(req)?;
|
||||
let token = "placeholder_token_to_remove"; // Replace with actual extraction
|
||||
|
||||
// 2. Remove session from state store
|
||||
let mut sessions = app_state.active_sessions.lock().unwrap();
|
||||
if sessions.remove(token).is_some() {
|
||||
log::info!("Logout: Removed session for token: {}", token);
|
||||
Ok(create_api_response::<()>(None, true, Some("Logged out successfully.".to_string())))
|
||||
} else {
|
||||
log::warn!("Logout: Attempted to log out with invalid or expired token: {}", token);
|
||||
// Still return success, as the user is effectively logged out from the server's perspective.
|
||||
Ok(create_api_response::<()>(None, true, Some("Logged out successfully.".to_string())))
|
||||
// Or return an error if strict feedback is needed:
|
||||
// Err(AppError::AuthenticationError("Invalid session token.".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add get_profile handler (needs token validation middleware)
|
||||
#[get("/profile/{user_id}")]
|
||||
async fn get_profile(
|
||||
pool: web::Data>,
|
||||
app_state: web::Data, // Needed if validating token here
|
||||
path: web::Path,
|
||||
// TODO: Add middleware to extract validated user info from token
|
||||
// validated_user: AuthenticatedUser, // Example struct from middleware
|
||||
) -> Result {
|
||||
let requested_user_id_str = path.into_inner();
|
||||
let requested_user_id = parse_user_id(&requested_user_id_str)?;
|
||||
|
||||
// --- Authorization ---
|
||||
// TODO: Check if the requesting user (from token) is allowed to see this profile.
|
||||
// Example: Allow viewing own profile or if requester is admin.
|
||||
// if validated_user.user_id != requested_user_id_str && !validated_user.is_admin {
|
||||
// return Err(AppError::AuthorizationError("You are not authorized to view this profile.".to_string()));
|
||||
// }
|
||||
|
||||
// Fetch student or teacher data
|
||||
let student = db::find_student_by_id(&pool, requested_user_id).await?;
|
||||
let teacher = db::find_teacher_by_id(&pool, requested_user_id).await?;
|
||||
|
||||
match (student, teacher) {
|
||||
(Some(s), _) => { // Prioritize student if ID overlaps? Or check based on validated_user.user_type?
|
||||
let profile_data = ProfileData {
|
||||
user_id: s.user_id.to_string(),
|
||||
full_name: format!("{} {}", s.first_name, s.last_name),
|
||||
profile_picture: s.profile_picture,
|
||||
school_id: s.school_id,
|
||||
// Only show birthdate if viewing own profile (needs auth check)
|
||||
birthdate: s.birthday.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||
user_type: "student".to_string(),
|
||||
};
|
||||
Ok(create_api_response(Some(profile_data), true, None))
|
||||
},
|
||||
(None, Some(t)) => {
|
||||
let profile_data = ProfileData {
|
||||
user_id: t.user_id.to_string(),
|
||||
full_name: format!("{} {}", t.first_name, t.last_name),
|
||||
profile_picture: t.profile_picture,
|
||||
school_id: None,
|
||||
birthdate: t.birthday.map(|d| d.format("%Y-%m-%d").to_string()),
|
||||
user_type: "teacher".to_string(),
|
||||
};
|
||||
Ok(create_api_response(Some(profile_data), true, None))
|
||||
},
|
||||
(None, None) => Err(AppError::NotFound(format!("User profile {} not found.", requested_user_id_str))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add update_account_settings handler (needs token validation middleware)
|
||||
#[put("/profile/settings")]
|
||||
async fn update_account_settings(
|
||||
pool: web::Data>,
|
||||
// validated_user: AuthenticatedUser, // From middleware
|
||||
payload: web::Json,
|
||||
) -> Result {
|
||||
// 1. Get user_id and user_type from validated_user token
|
||||
// let user_id = parse_user_id(&validated_user.user_id)?;
|
||||
// let user_type = &validated_user.user_type;
|
||||
|
||||
// --- Mocked user for now ---
|
||||
let user_id = 123456; // Example user ID
|
||||
let user_type = "student"; // Example user type
|
||||
// --- ---
|
||||
|
||||
log::info!("Updating settings for {} ID: {}", user_type, user_id);
|
||||
|
||||
if let Some(pic) = &payload.profile_picture {
|
||||
match user_type {
|
||||
"student" => {
|
||||
sqlx::query!("UPDATE students SET profile_picture = ? WHERE user_id = ?", pic, user_id)
|
||||
.execute(&**pool).await?;
|
||||
},
|
||||
"teacher" => {
|
||||
sqlx::query!("UPDATE teachers SET profile_picture = ? WHERE user_id = ?", pic, user_id)
|
||||
.execute(&**pool).await?;
|
||||
},
|
||||
_ => return Err(AppError::InternalError("Invalid user type.".to_string())),
|
||||
}
|
||||
log::info!("Updated profile picture for {} ID: {}", user_type, user_id);
|
||||
}
|
||||
|
||||
// Handle other updatable fields (e.g., non-sensitive ones)
|
||||
// Password change requires a separate, secure flow (e.g., SRP re-registration or old/new password check)
|
||||
|
||||
Ok(create_api_response::<()>(None, true, Some("Settings updated successfully.".to_string())))
|
||||
}
|
||||
|
||||
|
||||
// --- Admin Handlers ---
|
||||
|
||||
#[get("/admin/dashboard")]
|
||||
async fn get_admin_dashboard_data(pool: web::Data>, /* TODO: Admin Auth Middleware */) -> Result {
|
||||
let students_count = db::count_students(&pool).await?;
|
||||
let teachers_count = db::count_teachers(&pool).await?;
|
||||
|
||||
let data = AdminDashboardData {
|
||||
students_count,
|
||||
teachers_count,
|
||||
};
|
||||
Ok(create_api_response(Some(data), true, None))
|
||||
}
|
||||
|
||||
#[get("/admin/students")]
|
||||
async fn get_student_list(pool: web::Data>, /* TODO: Admin Auth Middleware */) -> Result {
|
||||
// Basic implementation: get all students, construct name.
|
||||
// TODO: Add filtering, pagination, course info joins.
|
||||
let students = sqlx::query_as::<_, StudentListData>(
|
||||
r#"
|
||||
SELECT
|
||||
user_id as id,
|
||||
CONCAT(first_name, ' ', last_name) as name
|
||||
-- Add columns for year_level, courses (requires more complex query/joins)
|
||||
FROM students
|
||||
ORDER BY last_name, first_name
|
||||
"#
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(create_api_response(Some(students), true, None))
|
||||
}
|
||||
|
||||
#[get("/admin/students/{student_id}/financials")]
|
||||
async fn get_student_financial_data(
|
||||
_pool: web::Data>, // Mark as unused for now
|
||||
path: web::Path,
|
||||
/* TODO: Admin Auth Middleware */
|
||||
) -> Result {
|
||||
let student_id_str = path.into_inner();
|
||||
let _student_id = parse_user_id(&student_id_str)?; // Validate ID format
|
||||
|
||||
// TODO: Check if student_id exists
|
||||
|
||||
log::info!("Fetching financial data for student ID: {}", _student_id);
|
||||
// Mock data as per frontend api.ts, since no financial table exists
|
||||
let financial_data = StudentFinancialData {
|
||||
tuition_fee: "5000".to_string(),
|
||||
miscellaneous_fee: "500".to_string(),
|
||||
lab_fee: "200".to_string(),
|
||||
current_account: "-100".to_string(),
|
||||
down_payment: "2000".to_string(),
|
||||
midterm_payment: "1500".to_string(),
|
||||
prefinal_payment: "1000".to_string(),
|
||||
final_payment: "500".to_string(),
|
||||
};
|
||||
|
||||
Ok(create_api_response(Some(financial_data), true, None))
|
||||
}
|
||||
|
||||
|
||||
// --- Configure Routes ---
|
||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
// Initialize App State (Singleton for the App)
|
||||
let app_state = web::Data::new(AppState::new());
|
||||
|
||||
|
||||
cfg.app_data(app_state.clone()) // Make AppState available to handlers
|
||||
.service(health_check)
|
||||
.service(
|
||||
web::scope("/api")
|
||||
// Authentication (No auth middleware needed)
|
||||
.service(
|
||||
web::scope("/auth")
|
||||
.service(srp_login_start)
|
||||
.service(srp_login_verify)
|
||||
.service(logout) // TODO: Add auth middleware if logout requires token
|
||||
)
|
||||
// Profile routes (TODO: Add auth middleware)
|
||||
.service(
|
||||
web::scope("/profile")
|
||||
.service(get_profile) // Dynamic part {user_id}
|
||||
.service(update_account_settings) // Path defined in handler
|
||||
)
|
||||
// Admin routes (TODO: Add admin auth middleware)
|
||||
.service(
|
||||
web::scope("/admin")
|
||||
.service(get_admin_dashboard_data)
|
||||
.service(get_student_list)
|
||||
.service(get_student_financial_data) // Dynamic part {student_id}
|
||||
// TODO: Add routes for managing students/teachers/courses/enrollments
|
||||
)
|
||||
// TODO: Add routes for classrooms, posts, etc.
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement Authentication Middleware
|
||||
// - Extract token from "Authorization: Bearer " header.
|
||||
// - Verify token against the active_sessions store (or JWT verification).
|
||||
// - If valid, extract user info (user_id, user_type) and attach it to the request extensions.
|
||||
// - If invalid, return 401 Unauthorized.
|
||||
|
||||
// TODO: Implement Authorization Middleware (or checks within handlers)
|
||||
// - Check if the authenticated user has the necessary role/permissions (e.g., admin) for the requested route.
|
||||
// - If not authorized, return 403 Forbidden.
|
||||
|
||||
// TODO: Implement SRP Registration/Password Change Handler
|
||||
// - Takes username and password.
|
||||
// - Generates salt and verifier using srp::server::generate_verifier.
|
||||
// - Stores hex-encoded salt and verifier in the appropriate (student/teacher) table.
|
||||
// - This is needed for users to set their initial password or change it.
|
||||
62
src/main.rs
Normal file
62
src/main.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{middleware::Logger, web, App, HttpServer};
|
||||
use dotenv::dotenv;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use std::env;
|
||||
|
||||
mod config;
|
||||
mod db;
|
||||
mod errors;
|
||||
mod handlers;
|
||||
mod models;
|
||||
// mod auth; // Keep auth logic within handlers for now or create dedicated module
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok(); // Load .env file
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); // Use RUST_LOG from .env
|
||||
|
||||
let app_config = config::Config::from_env().expect("Failed to load configuration");
|
||||
|
||||
log::info!("Connecting to database: {}", &app_config.database_url);
|
||||
let db_pool = MySqlPoolOptions::new()
|
||||
.max_connections(10)
|
||||
.connect(&app_config.database_url)
|
||||
.await
|
||||
.expect("Failed to create database connection pool");
|
||||
|
||||
log::info!("Running database migrations/setup...");
|
||||
db::init_db(&db_pool)
|
||||
.await
|
||||
.expect("Database initialization failed");
|
||||
log::info!("Database setup complete.");
|
||||
|
||||
// Optional: Populate initial data if needed (courses, etc.)
|
||||
// Ensure this is idempotent or only runs once
|
||||
if let Err(e) = db::populate_initial_data(&db_pool).await {
|
||||
log::warn!("Could not populate initial data (might already exist): {}", e);
|
||||
} else {
|
||||
log::info!("Initial data populated (if tables were empty).");
|
||||
}
|
||||
|
||||
|
||||
log::info!("Starting server at {}", &app_config.server_addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin() // Adjust for production!
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(db_pool.clone()))
|
||||
// .app_data(web::Data::new(app_config.clone())) // If config needed in handlers
|
||||
.wrap(Logger::default()) // Enable request logging
|
||||
.wrap(cors) // Enable CORS
|
||||
.configure(handlers::configure_routes) // Register API routes
|
||||
})
|
||||
.bind(&app_config.server_addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
166
src/models.rs
Normal file
166
src/models.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use chrono::NaiveDate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
// --- Database Table Models ---
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Student {
|
||||
pub user_id: i32, // Assuming INT translates to i32
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub birthday: Option, // Use Option if nullable
|
||||
// pub password_hash: String, // Replaced by SRP fields
|
||||
pub srp_salt: Option, // Store as hex string or base64
|
||||
pub srp_verifier: Option, // Store as hex string or base64
|
||||
// Add other fields like school_id if needed based on frontend expectations
|
||||
pub school_id: Option, // Matches frontend LoginResponseData
|
||||
pub profile_picture: Option, // Matches frontend LoginResponseData
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Teacher {
|
||||
pub user_id: i32,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub birthday: Option,
|
||||
// pub password_hash: String, // Replaced by SRP fields
|
||||
pub srp_salt: Option,
|
||||
pub srp_verifier: Option,
|
||||
// 'enrolled' was listed as a list of courses, this needs a join table (Enrollments)
|
||||
// This struct represents the core teacher info. Enrollment is handled separately.
|
||||
pub profile_picture: Option, // Matches frontend LoginResponseData (assumption)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Course {
|
||||
pub course_code: i32, // As per spec, though VARCHAR might be better
|
||||
pub course_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Enrollment {
|
||||
pub student_id: i32,
|
||||
pub course_id: i32,
|
||||
pub enrollment_date: Option,
|
||||
}
|
||||
|
||||
// --- API Request/Response Structs (Matching frontend's api.ts where possible) ---
|
||||
|
||||
// Generic API Response Wrapper
|
||||
#[derive(Serialize)]
|
||||
pub struct ApiResponse {
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option,
|
||||
}
|
||||
|
||||
// Data structure for successful login (after SRP verification)
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginSuccessData {
|
||||
pub user_id: String, // Use String to match frontend potentially using non-numeric IDs? Keep consistent.
|
||||
pub full_name: String,
|
||||
pub profile_picture: Option, // Allow null/optional
|
||||
pub school_id: Option, // Allow null/optional
|
||||
pub birthdate: Option, // Format as YYYY-MM-DD string
|
||||
pub token: String, // Session token (e.g., UUID or JWT)
|
||||
pub user_type: String, // "student" or "teacher"
|
||||
}
|
||||
|
||||
// Data structure for profile page
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProfileData {
|
||||
pub user_id: String,
|
||||
pub full_name: String,
|
||||
pub profile_picture: Option,
|
||||
pub school_id: Option,
|
||||
pub birthdate: Option, // Only show birthdate if viewing own profile? Needs logic.
|
||||
// Add posts if implementing that feature
|
||||
// pub posts: Vec,
|
||||
pub user_type: String, // "student" or "teacher"
|
||||
}
|
||||
|
||||
// Placeholder for Post data if needed later
|
||||
// #[derive(Debug, Serialize)]
|
||||
// pub struct PostData {
|
||||
// pub id: i32,
|
||||
// pub content: String,
|
||||
// }
|
||||
|
||||
|
||||
// Admin Dashboard Data
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminDashboardData {
|
||||
pub students_count: i64, // Use i64 for counts from SQL COUNT(*)
|
||||
pub teachers_count: i64,
|
||||
}
|
||||
|
||||
// Student List Data for Admin Page
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StudentListData {
|
||||
pub id: i32, // Assuming user_id is the ID here
|
||||
pub name: String, // Need to construct this (first_name + last_name) in query
|
||||
// These fields need to be fetched/calculated, perhaps via joins or separate queries
|
||||
// pub year_level: Option, // Not in original schema
|
||||
// pub courses: Vec, // Need to query Enrollments and Courses
|
||||
}
|
||||
|
||||
// Student Financial Data (Mocked as per frontend)
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StudentFinancialData {
|
||||
pub tuition_fee: String,
|
||||
pub miscellaneous_fee: String,
|
||||
pub lab_fee: String,
|
||||
pub current_account: String,
|
||||
pub down_payment: String,
|
||||
pub midterm_payment: String,
|
||||
pub prefinal_payment: String,
|
||||
pub final_payment: String,
|
||||
}
|
||||
|
||||
|
||||
// --- SRP Related Structs ---
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SrpLoginStartRequest {
|
||||
pub user_id: String, // User identifier (I)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SrpLoginStartResponse {
|
||||
pub salt: String, // Hex encoded salt (s)
|
||||
pub server_public_ephemeral: String, // Hex encoded server public ephemeral (B)
|
||||
pub user_type: String, // "student" or "teacher" (needed for verify step)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SrpLoginVerifyRequest {
|
||||
pub user_id: String, // Pass user_id again to correlate state if needed
|
||||
pub client_public_ephemeral: String, // Hex encoded client public ephemeral (A)
|
||||
pub client_proof: String, // Hex encoded client proof (M1)
|
||||
pub user_type: String, // "student" or "teacher"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SrpLoginVerifyResponse {
|
||||
pub server_proof: String, // Hex encoded server proof (M2)
|
||||
// On successful verification, include the LoginSuccessData
|
||||
#[serde(flatten)] // Embed fields directly
|
||||
pub login_data: Option, // Only present on success
|
||||
}
|
||||
|
||||
// --- Other API Structs ---
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAccountSettingsPayload {
|
||||
pub profile_picture: Option,
|
||||
// Password change needs separate secure flow (e.g., old+new password or SRP re-registration)
|
||||
// pub new_password: Option,
|
||||
// Other editable fields?
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user