Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
1fe01c2
fixed some things
GtJohnny May 12, 2025
68955d1
documentation
GtJohnny May 12, 2025
e2b46c3
another line
GtJohnny May 12, 2025
6b5700d
Summary for controller
GtJohnny May 12, 2025
dfe469d
indeed
GtJohnny May 12, 2025
9bd95fa
swagger
GtJohnny May 12, 2025
70bac8c
Merge pull request #39 from Watergirll/main
whos-gabi May 14, 2025
b08d242
Merge pull request #41 from Watergirll/main
whos-gabi May 20, 2025
99ef9d4
Merge branch 'main' into 34-code-standards
GtJohnny Nov 2, 2025
f8258be
Merge pull request #37 from DriveFlow-CRM/34-code-standards
whos-gabi Nov 2, 2025
961e23f
Add exam and session form management with migrations
ForceOfNature13 Dec 20, 2025
aef90d4
Merge pull request #45 from ForceOfNature13/main
ForceOfNature13 Dec 20, 2025
c84398a
Expand SeedData to include comprehensive test data
ForceOfNature13 Dec 20, 2025
bd03fc0
Merge branch 'main' of https://github.com/ForceOfNature13/DriveFlow-C…
ForceOfNature13 Dec 20, 2025
65774e2
Merge pull request #46 from ForceOfNature13/main
ForceOfNature13 Dec 20, 2025
14c9376
Add session form history API and detailed view endpoints
ForceOfNature13 Dec 20, 2025
f4ff5c7
Merge pull request #47 from ForceOfNature13/main
ForceOfNature13 Dec 20, 2025
9dd84e3
sample.env
whos-gabi Jan 4, 2026
6859fdb
SeedData modified
GtJohnny Jan 18, 2026
1870540
deserialization
GtJohnny Jan 18, 2026
656eb2c
Cohort
GtJohnny Jan 18, 2026
12affc6
added rights
GtJohnny Jan 19, 2026
a1291c0
studentStats
GtJohnny Jan 19, 2026
0a9a254
documentation
GtJohnny Jan 19, 2026
24f3a89
kpis
GtJohnny Jan 20, 2026
fd0bf93
auth
GtJohnny Jan 20, 2026
07318ef
tested kpi
GtJohnny Jan 20, 2026
97db675
Merge pull request #48 from DriveFlow-CRM/InstructorStats
whos-gabi Jan 20, 2026
cc2fb47
Merge pull request #49 from DriveFlow-CRM/StudentStatistics
whos-gabi Jan 20, 2026
ce990ae
Merge pull request #50 from DriveFlow-CRM/KPI
whos-gabi Jan 20, 2026
c82bfa2
Fix Dockerfile path and seed/migrations
whos-gabi Jan 23, 2026
124a04a
expand seed data for multi-school demo
whos-gabi Jan 23, 2026
b10a92a
rename db env var to DB_CONNECTION_URI
whos-gabi Jan 23, 2026
278328e
remove heroku config
whos-gabi Jan 23, 2026
b167467
Merge pull request #52 from DriveFlow-CRM/dev
whos-gabi Jan 23, 2026
c733b2c
feat: add optional fileId parameter to stats/mistakes and session-for…
Watergirll Jan 26, 2026
734001a
Merge pull request #57 from DriveFlow-CRM/issue-55
Watergirll Jan 26, 2026
66f4889
Merge pull request #58 from DriveFlow-CRM/dev
Watergirll Jan 26, 2026
e233d72
fix: remove temporary ResetDb code from Program.cs
Watergirll Jan 26, 2026
37c2c6e
Merge pull request #59 from DriveFlow-CRM/dev
Watergirll Jan 26, 2026
4518994
fix: restrict /api/forms/seed route to SuperAdmin only
Watergirll Jan 26, 2026
1dcd4f5
Merge pull request #60 from DriveFlow-CRM/issue-54
Watergirll Jan 26, 2026
e505eda
Merge pull request #61 from DriveFlow-CRM/dev
Watergirll Jan 26, 2026
eae48bf
feat: add fileId to fetchInstructorAssignedFiles response
Watergirll Jan 26, 2026
3e12b35
Merge pull request #62 from DriveFlow-CRM/dev
Watergirll Jan 26, 2026
7ad1ffa
feat: refactor session forms, add studentId to instructor files
Watergirll Jan 26, 2026
4b7929d
Merge pull request #63 from DriveFlow-CRM/issue-53
Watergirll Jan 26, 2026
a83fe9f
Merge pull request #64 from DriveFlow-CRM/dev
Watergirll Jan 26, 2026
7ebbf8f
added context for chatai
Watergirll Jan 27, 2026
b749b0b
Merge pull request #65 from DriveFlow-CRM/feat-chatcntxt
Watergirll Jan 27, 2026
a7a8860
Merge pull request #66 from DriveFlow-CRM/dev
Watergirll Jan 27, 2026
32d32f9
Link ExamForm to License, add connection resilience, add LicenseId to…
Watergirll Jan 28, 2026
84963cd
Merge pull request #68 from DriveFlow-CRM/feat-examform-license-link
Watergirll Jan 28, 2026
ddd977f
Merge pull request #69 from DriveFlow-CRM/dev
Watergirll Jan 28, 2026
f737d99
feat(ai): add AI chat streaming proxy endpoint
whos-gabi Jan 28, 2026
9a892e8
Merge pull request #70 from DriveFlow-CRM/feature/ai-chat-streaming-p…
whos-gabi Jan 28, 2026
7ce3235
Merge pull request #71 from DriveFlow-CRM/dev
whos-gabi Jan 28, 2026
4e895c6
Add comprehensive unit and system tests, improve auth and CORS
ForceOfNature13 Jan 31, 2026
10a2698
Merge pull request #72 from ForceOfNature13/feature/tests-security
ForceOfNature13 Jan 31, 2026
f955d18
Merge pull request #73 from DriveFlow-CRM/dev
ForceOfNature13 Jan 31, 2026
dd2abc4
fix(cors): allow all origins with credentials support
whos-gabi Jan 31, 2026
7ab10c8
Merge pull request #74 from DriveFlow-CRM/dev
whos-gabi Jan 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 && \
Expand Down
10 changes: 10 additions & 0 deletions DriveFlow-CRM-API/Auth/SameSchoolHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ protected override Task HandleRequirementAsync(
return Task.CompletedTask;
}
}










198 changes: 198 additions & 0 deletions DriveFlow-CRM-API/Controllers/AiController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// AI chat endpoints for the DriveFlow CRM API.
/// Provides a secure proxy to OpenRouter API with student context injection.
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
[ApiController]
[Route("api/ai")]
public class AiController : ControllerBase
{
private readonly IAiContextBuilder _contextBuilder;
private readonly IAiStreamingService _streamingService;

/// <summary>
/// Constructor injected by the framework with request-scoped services.
/// </summary>
public AiController(
IAiContextBuilder contextBuilder,
IAiStreamingService streamingService)
{
_contextBuilder = contextBuilder;
_streamingService = streamingService;
}

/// <summary>
/// Streams AI chat responses for the authenticated student via Server-Sent Events (SSE).
/// </summary>
/// <remarks>
/// 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
///
/// <para><strong>Authorization:</strong></para>
/// - Only students can access this endpoint
/// - Student context is built server-side using the authenticated user's ID
///
/// <para><strong>Request body:</strong></para>
/// <code>
/// {
/// "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)
/// }
/// </code>
///
/// <para><strong>SSE Response format:</strong></para>
/// <code>
/// event: chunk
/// data: Bună
///
/// event: chunk
/// data: , Ion!
///
/// event: chunk
/// data: Văd că ai
///
/// event: done
/// data:
/// </code>
///
/// <para><strong>Error event:</strong></para>
/// <code>
/// event: error
/// data: {"message": "Failed to connect to AI service"}
/// </code>
///
/// <para><strong>Conversation flow:</strong></para>
/// - 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
/// </remarks>
/// <param name="request">Chat request with conversation messages</param>
/// <response code="200">SSE stream started successfully</response>
/// <response code="401">User is not authenticated</response>
/// <response code="403">User is not a student</response>
/// <response code="404">Student not found in database</response>
[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<object>
{
new { role = "system", content = context.SystemPrompt },
new { role = "system", content = JsonSerializer.Serialize(context.Context) }
};

// Add conversation history from request
if (request.Messages != null)
{
messages.AddRange(request.Messages.Select(m => new
{
role = m.Role,
content = m.Content
}));
}

// 5. Set SSE headers
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
Response.Headers.Append("X-Accel-Buffering", "no"); // Disable nginx buffering

// 6. Stream response from OpenRouter to client
await _streamingService.StreamToClientAsync(
messages,
Response,
HttpContext.RequestAborted);
}

/// <summary>
/// Health check endpoint for the AI chat service.
/// </summary>
/// <remarks>
/// Simple endpoint to verify the AI chat service is available.
/// Does not require authentication.
/// </remarks>
/// <response code="200">Service is healthy</response>
[HttpGet("health")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<object> HealthCheck()
{
// Check if OpenRouter is configured
var apiKeyConfigured = !string.IsNullOrEmpty(
Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"));

return Ok(new
{
status = "healthy",
service = "ai-chat",
timestamp = DateTime.UtcNow,
version = "2.0.0",
openRouterConfigured = apiKeyConfigured
});
}
}
36 changes: 28 additions & 8 deletions DriveFlow-CRM-API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class AuthController : ControllerBase
private readonly ITokenGenerator _tok; // Access‑token generator
private readonly IRefreshTokenService _rtok; // Refresh‑token persistence
private readonly IConfiguration _cfg;
private readonly ILogger<AuthController> _logger;

/// <summary>
/// Constructor injected by the framework with request‑scoped services.
Expand All @@ -49,13 +50,15 @@ public AuthController(
SignInManager<ApplicationUser> signIn,
ITokenGenerator tok,
IRefreshTokenService rtok,
IConfiguration cfg)
IConfiguration cfg,
ILogger<AuthController> logger)
{
_users = users;
_signIn = signIn;
_tok = tok;
_rtok = rtok;
_cfg = cfg;
_logger = logger;
}

// ───────────────────────── LOGIN ─────────────────────────
Expand Down Expand Up @@ -92,28 +95,45 @@ public async Task<IActionResult> LoginAsync(LoginDto dto)
if (user is null)
return NotFound(new { error = 404, message = "Account not found!" });

// 2. Verify password
var valid = await _signIn.CheckPasswordSignInAsync(user, dto.Password, lockoutOnFailure: false);
// 2. Check if user is locked out
if (await _users.IsLockedOutAsync(user))
{
var lockoutEnd = await _users.GetLockoutEndDateAsync(user);
return StatusCode(423, new
{
error = 423,
message = "Account is temporarily locked due to multiple failed login attempts.",
lockoutEnd = lockoutEnd?.UtcDateTime
});
}

// 3. Verify password (lockoutOnFailure: true enables account lockout after failed attempts)
var valid = await _signIn.CheckPasswordSignInAsync(user, dto.Password, lockoutOnFailure: true);
if (!valid.Succeeded)
{
_logger.LogWarning("Failed login attempt for user {Email} from IP {IP}",
dto.Email,
HttpContext.Connection.RemoteIpAddress);
return Unauthorized(new { error = 401, message = "Incorrect email or password!" });
}

// 3. Fetch the single role assigned to this account
// 4. Fetch the single role assigned to this account
var roles = await _users.GetRolesAsync(user);
var role = roles.Count > 0 ? roles[0] : "Student"; // Fallback for safety

// 4. Domain‑specific data (placeholder until school relationship exists)
// 5. Domain‑specific data (placeholder until school relationship exists)
var schoolId = user.AutoSchoolId ?? 0;

// 5. Generate ACCESS token
// 6. Generate ACCESS token
var accessTok = _tok.GenerateToken(user, roles, schoolId);

// 6. Generate REFRESH token (can use different lifetime / key)
// 7. Generate REFRESH token (can use different lifetime / key)
var refreshGen = TokenGeneratorFactory.Create(TokenType.Refresh, HttpContext.RequestServices);
var refreshTok = refreshGen.GenerateToken(user, roles, schoolId);
var refreshExp = DateTime.UtcNow.AddDays(int.Parse(_cfg["Jwt:RefreshExpiresDays"]!));
await _rtok.StoreAsync(user, refreshTok, refreshExp);

// 7. Build response DTO
// 8. Build response DTO
var response = new
{
token = accessTok,
Expand Down
Loading