diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..6a15575
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,20 @@
+name: Deploy df-api
+
+on:
+ push:
+ branches: ["main"]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Deploy df-api on VPS
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.VPS_HOST }}
+ username: ${{ secrets.VPS_USER }}
+ key: ${{ secrets.VPS_SSH_KEY }}
+ port: ${{ secrets.VPS_PORT }}
+ script: |
+ cd "${{ secrets.VPS_APP_PATH }}"
+ ./scripts/deploy.sh df-api
diff --git a/Dockerfile b/Dockerfile
index 0120dbe..de95a0c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0.407 AS build
WORKDIR /source
-# Copy only the API project
-COPY DriveFlow-CRM-API/ ./
+# Copy only the API project into its folder
+COPY DriveFlow-CRM-API/ ./DriveFlow-CRM-API/
+
+# Move into the actual project directory (contains the csproj)
+WORKDIR /source/DriveFlow-CRM-API
# Configure NuGet properly
RUN mkdir -p /root/.nuget/NuGet && \
diff --git a/DriveFlow-CRM-API/Auth/SameSchoolHandler.cs b/DriveFlow-CRM-API/Auth/SameSchoolHandler.cs
index 3dd6ab9..ce82c24 100644
--- a/DriveFlow-CRM-API/Auth/SameSchoolHandler.cs
+++ b/DriveFlow-CRM-API/Auth/SameSchoolHandler.cs
@@ -36,3 +36,13 @@ protected override Task HandleRequirementAsync(
return Task.CompletedTask;
}
}
+
+
+
+
+
+
+
+
+
+
diff --git a/DriveFlow-CRM-API/Controllers/AiController.cs b/DriveFlow-CRM-API/Controllers/AiController.cs
new file mode 100644
index 0000000..ccf746b
--- /dev/null
+++ b/DriveFlow-CRM-API/Controllers/AiController.cs
@@ -0,0 +1,198 @@
+using DriveFlow_CRM_API.Models.DTOs;
+using DriveFlow_CRM_API.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System.Security.Claims;
+using System.Text.Json;
+
+namespace DriveFlow_CRM_API.Controllers;
+
+///
+/// AI chat endpoints for the DriveFlow CRM API.
+/// Provides a secure proxy to OpenRouter API with student context injection.
+///
+///
+/// This controller:
+/// - Builds student context server-side from the database
+/// - Calls OpenRouter API with the context and user messages
+/// - Streams responses back via Server-Sent Events (SSE)
+/// - Keeps the API key secure on the backend (never exposed to frontend)
+///
+[ApiController]
+[Route("api/ai")]
+public class AiController : ControllerBase
+{
+ private readonly IAiContextBuilder _contextBuilder;
+ private readonly IAiStreamingService _streamingService;
+
+ ///
+ /// Constructor injected by the framework with request-scoped services.
+ ///
+ public AiController(
+ IAiContextBuilder contextBuilder,
+ IAiStreamingService streamingService)
+ {
+ _contextBuilder = contextBuilder;
+ _streamingService = streamingService;
+ }
+
+ ///
+ /// Streams AI chat responses for the authenticated student via Server-Sent Events (SSE).
+ ///
+ ///
+ /// This endpoint:
+ /// 1. Builds student context from the database (progress, mistakes, coaching notes)
+ /// 2. Prepends context as system messages to the conversation
+ /// 3. Calls OpenRouter API with streaming enabled
+ /// 4. Streams the response back to the client via SSE
+ ///
+ /// Authorization:
+ /// - Only students can access this endpoint
+ /// - Student context is built server-side using the authenticated user's ID
+ ///
+ /// Request body:
+ ///
+ /// {
+ /// "messages": [
+ /// { "role": "user", "content": "Cum mă pot pregăti mai bine?" },
+ /// { "role": "assistant", "content": "Bună! Văd că ai..." },
+ /// { "role": "user", "content": "Ce greșeli fac cel mai des?" }
+ /// ],
+ /// "historySessions": 5, // Optional, default 5 (sessions per category for context)
+ /// "language": "ro" // Optional, default "ro" (system prompt language)
+ /// }
+ ///
+ ///
+ /// SSE Response format:
+ ///
+ /// event: chunk
+ /// data: Bună
+ ///
+ /// event: chunk
+ /// data: , Ion!
+ ///
+ /// event: chunk
+ /// data: Văd că ai
+ ///
+ /// event: done
+ /// data:
+ ///
+ ///
+ /// Error event:
+ ///
+ /// event: error
+ /// data: {"message": "Failed to connect to AI service"}
+ ///
+ ///
+ /// Conversation flow:
+ /// - Frontend sends full conversation history with each request
+ /// - Backend prepends fresh context (from database) on every request
+ /// - Context is always up-to-date with student's latest progress
+ ///
+ /// Chat request with conversation messages
+ /// SSE stream started successfully
+ /// User is not authenticated
+ /// User is not a student
+ /// Student not found in database
+ [HttpPost("chat/stream")]
+ [Authorize(Roles = "Student")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task StreamChat([FromBody] ChatRequest request)
+ {
+ // 1. Get authenticated user's ID from JWT claims
+ var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
+ if (string.IsNullOrEmpty(userId))
+ {
+ Response.StatusCode = StatusCodes.Status401Unauthorized;
+ await Response.WriteAsJsonAsync(new { message = "User not authenticated" });
+ return;
+ }
+
+ // 2. Verify the user has Student role (additional check beyond [Authorize])
+ if (!User.IsInRole("Student"))
+ {
+ Response.StatusCode = StatusCodes.Status403Forbidden;
+ await Response.WriteAsJsonAsync(new { message = "Access denied" });
+ return;
+ }
+
+ // 3. Build student context from database
+ var historySessions = request.HistorySessions ?? 5;
+ var language = request.Language ?? "ro";
+
+ var context = await _contextBuilder.BuildStudentContextAsync(
+ userId,
+ historySessions,
+ language,
+ HttpContext.RequestAborted);
+
+ if (context == null)
+ {
+ Response.StatusCode = StatusCodes.Status404NotFound;
+ await Response.WriteAsJsonAsync(new { message = "Student not found" });
+ return;
+ }
+
+ // 4. Prepare messages for OpenRouter
+ // - Message 1 (system): The system prompt with AI behavior instructions
+ // - Message 2 (system): The student context as JSON
+ // - Messages 3+: The conversation history from the request
+ var messages = new List