diff --git a/api/Configuration/ConfigHelper.cs b/api/Configuration/ConfigHelper.cs
new file mode 100644
index 0000000..4c3e7f1
--- /dev/null
+++ b/api/Configuration/ConfigHelper.cs
@@ -0,0 +1,6 @@
+namespace WinStudentGoalTracker.Api.Configuration;
+
+public static class ConfigHelper
+{
+ public static IConfiguration Configuration { get; set; } = null!;
+}
diff --git a/api/Program.cs b/api/Program.cs
index 3917ef1..c4dff81 100644
--- a/api/Program.cs
+++ b/api/Program.cs
@@ -1,41 +1,30 @@
-var builder = WebApplication.CreateBuilder(args);
-
-// Add services to the container.
-// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
-builder.Services.AddOpenApi();
-
-var app = builder.Build();
-
-// Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
-{
- app.MapOpenApi();
-}
-
-app.UseHttpsRedirection();
-
-var summaries = new[]
-{
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
-};
-
-app.MapGet("/weatherforecast", () =>
-{
- var forecast = Enumerable.Range(1, 5).Select(index =>
- new WeatherForecast
- (
- DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
- Random.Shared.Next(-20, 55),
- summaries[Random.Shared.Next(summaries.Length)]
- ))
- .ToArray();
- return forecast;
-})
-.WithName("GetWeatherForecast");
-
-app.Run();
-
-record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
-{
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-}
+using WinStudentGoalTracker.Api.Configuration;
+
+var builder = WebApplication.CreateBuilder(args);
+
+ConfigHelper.Configuration = builder.Configuration;
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+builder.Services.AddCors(options =>
+{
+ options.AddDefaultPolicy(policy =>
+ {
+ policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
+ });
+});
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseCors();
+app.UseHttpsRedirection();
+app.MapControllers();
+
+app.Run();
diff --git a/api/api.csproj b/api/api.csproj
index d278206..4b8535c 100644
--- a/api/api.csproj
+++ b/api/api.csproj
@@ -1,13 +1,15 @@
-
-
-
- net9.0
- enable
- enable
-
-
-
-
-
-
-
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/api/api.http b/api/api.http
index f23a40a..d040041 100644
--- a/api/api.http
+++ b/api/api.http
@@ -1,6 +1,40 @@
-@api_HostAddress = http://localhost:5123
-
-GET {{api_HostAddress}}/weatherforecast/
-Accept: application/json
-
-###
+@api_HostAddress = http://localhost:5123
+
+GET {{api_HostAddress}}/api/student
+Accept: application/json
+
+###
+
+GET {{api_HostAddress}}/api/student/1001
+Accept: application/json
+
+###
+
+POST {{api_HostAddress}}/api/student
+Content-Type: application/json
+
+{
+ "idStudent": 1001,
+ "idProgram": 10,
+ "identifier": "WIN-1001",
+ "programYear": 2026,
+ "enrollmentDate": "2026-01-15",
+ "expectedGrad": "2028-06-01"
+}
+
+###
+
+PUT {{api_HostAddress}}/api/student/1001
+Content-Type: application/json
+
+{
+ "identifier": "WIN-1001-A",
+ "programYear": 2027
+}
+
+###
+
+DELETE {{api_HostAddress}}/api/student/1001
+Accept: application/json
+
+###
diff --git a/api/appsettings.Development.json b/api/appsettings.Development.json
index ff66ba6..707cbc5 100644
--- a/api/appsettings.Development.json
+++ b/api/appsettings.Development.json
@@ -1,8 +1,11 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Port=3306;Database=win_student_goal_tracker;Uid=root;Pwd=change_me;"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
}
}
diff --git a/api/appsettings.json b/api/appsettings.json
index 4d56694..a76fc22 100644
--- a/api/appsettings.json
+++ b/api/appsettings.json
@@ -1,9 +1,12 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*"
-}
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Port=3306;Database=win_student_goal_tracker;Uid=root;Pwd=change_me;"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/api/dotnet-api-design-document.md b/api/design/dotnet-api-design-document.md
similarity index 100%
rename from api/dotnet-api-design-document.md
rename to api/design/dotnet-api-design-document.md
diff --git a/api/design/permission_structure.md b/api/design/permission_structure.md
new file mode 100644
index 0000000..e69de29
diff --git a/api/design/role-based-access-control.md b/api/design/role-based-access-control.md
new file mode 100644
index 0000000..d79ce71
--- /dev/null
+++ b/api/design/role-based-access-control.md
@@ -0,0 +1,499 @@
+# Role-Based Access Control Design Document
+
+## Student Goal & Progress Tracking Application
+
+---
+
+## 1. Overview
+
+This document defines the authorization model for the Student Goal & Progress Tracking application. The system uses a combination of role-based access control (RBAC) and resource-level assignment enforcement to determine what each user can do and to whom.
+
+All authorization decisions flow from two sources of truth:
+
+- **User role** — defines the category of actions a user may perform (e.g., Teacher, Paraeducator, Supervisor).
+- **Student assignments** — defines which students a user has access to and whether they hold primary responsibility.
+
+These two dimensions are evaluated together at every access point. A user's role determines what operations are available to them in general, and their assignment to a specific student determines whether they can perform those operations against that student's records.
+
+---
+
+## 2. Roles
+
+### 2.1 Teacher
+
+Teachers are the primary instructional staff responsible for student documentation.
+
+- May be assigned to multiple students.
+- May hold primary assignment for one or more students.
+- Primary assignment grants full control over the student's goals and records.
+- Non-primary assignment grants read access and the ability to add progress entries.
+
+### 2.2 Paraeducator
+
+Paraeducators are support staff who assist students in the field.
+
+- May be assigned to multiple students.
+- May add progress entries and critical notes for assigned students.
+- May edit or delete only entries they personally created.
+- Cannot create, edit, or archive goals.
+- Cannot view sensitive records unless explicitly permitted.
+
+### 2.3 Supervisor
+
+Supervisors are oversight users who review documentation for evaluation, audit, or legal purposes.
+
+- Have read-only access to all student records, entries, and reports.
+- Cannot create, modify, or delete any records.
+- Access is modeled through student assignments, ensuring uniform query behavior.
+
+---
+
+## 3. Student Assignments
+
+### 3.1 Design Principle
+
+The `student_assignments` table is the single source of truth for access control. Every user — regardless of role — must have an active assignment to a student in order to access that student's records. This eliminates role-based branching in queries and ensures that all access is explicit and auditable.
+
+### 3.2 Assignment Schema
+
+```sql
+CREATE TABLE student_assignments (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ student_id INT NOT NULL,
+ is_primary BOOLEAN NOT NULL DEFAULT FALSE,
+ start_date DATE NOT NULL,
+ end_date DATE NULL,
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ created_by INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users(id),
+ FOREIGN KEY (student_id) REFERENCES students(id)
+);
+```
+
+### 3.3 Field Definitions
+
+| Field | Description |
+|---|---|
+| `user_id` | The user being granted access. |
+| `student_id` | The student the user is being assigned to. |
+| `is_primary` | Whether this user holds primary responsibility for this student. Primary users may create and manage goals, edit the student profile, and view sensitive records. |
+| `start_date` | The date the assignment becomes effective. |
+| `end_date` | The date the assignment expires. NULL indicates an open-ended assignment. |
+| `is_active` | Whether the assignment is currently active. Supports manual deactivation independent of date range. |
+| `created_at` | Timestamp of assignment creation. |
+| `created_by` | The user who created the assignment. |
+
+### 3.4 Primary Assignment Rules
+
+- Only users with the Teacher role may hold a primary assignment (`is_primary = TRUE`).
+- A student should have exactly one primary assignment at any given time.
+- Paraeducators and Supervisors always have `is_primary = FALSE`.
+- The `is_primary` flag determines access to privileged operations such as goal management and student profile editing.
+
+### 3.5 Supervisor Assignment Strategy
+
+When a supervisor is added to the system, they receive an assignment record for each student in the program. When a new student is created, an assignment record is created for each active supervisor. This ensures supervisors are queryable through the same assignment-based access path as all other users, eliminating role-based branching in data access queries.
+
+---
+
+## 4. Permission Matrix
+
+The following matrix defines which operations are available to each role and assignment level. All operations require an active assignment to the relevant student.
+
+| Operation | Primary Teacher | Non-Primary Teacher | Paraeducator | Supervisor |
+|---|---|---|---|---|
+| View student profile | ✅ | ✅ | ✅ | ✅ |
+| Edit student profile | ✅ | ❌ | ❌ | ❌ |
+| Create goal | ✅ | ❌ | ❌ | ❌ |
+| Edit goal | ✅ | ❌ | ❌ | ❌ |
+| Archive goal | ✅ | ❌ | ❌ | ❌ |
+| Add progress entry | ✅ | ✅ | ✅ | ❌ |
+| Edit own progress entry | ✅ | ✅ | ✅ | ❌ |
+| Edit others' progress entry | ✅ | ❌ | ❌ | ❌ |
+| Delete own progress entry | ✅ | ✅ | ✅ | ❌ |
+| Delete others' progress entry | ✅ | ❌ | ❌ | ❌ |
+| Add critical note | ✅ | ✅ | ✅ | ❌ |
+| View sensitive records | ✅ | ❌ | ❌ | ❌ |
+| Generate report | ✅ | ✅ | ❌ | ✅ |
+
+---
+
+## 5. Authorization Architecture
+
+### 5.1 Approach
+
+The application uses ASP.NET Core's built-in policy-based and resource-based authorization framework. Authorization logic is centralized in handler classes rather than distributed across controllers or repositories.
+
+There are two complementary authorization strategies:
+
+- **Resource-based authorization** — used for single-entity endpoints. The controller loads or identifies the resource, then calls `IAuthorizationService.AuthorizeAsync` with a resource object. Registered handlers evaluate the user's role and assignment.
+- **Query-scoped access** — used for list endpoints. The query itself joins through the `student_assignments` table so that unauthorized records never leave the database.
+
+### 5.2 Operations
+
+Operations represent the vocabulary of actions the system can authorize. They are defined as static fields on a central `Operations` class.
+
+```csharp
+public static class Operations
+{
+ public static readonly OperationAuthorizationRequirement ViewStudent =
+ new() { Name = nameof(ViewStudent) };
+ public static readonly OperationAuthorizationRequirement EditStudent =
+ new() { Name = nameof(EditStudent) };
+ public static readonly OperationAuthorizationRequirement CreateGoal =
+ new() { Name = nameof(CreateGoal) };
+ public static readonly OperationAuthorizationRequirement EditGoal =
+ new() { Name = nameof(EditGoal) };
+ public static readonly OperationAuthorizationRequirement ArchiveGoal =
+ new() { Name = nameof(ArchiveGoal) };
+ public static readonly OperationAuthorizationRequirement AddProgressEntry =
+ new() { Name = nameof(AddProgressEntry) };
+ public static readonly OperationAuthorizationRequirement EditProgressEntry =
+ new() { Name = nameof(EditProgressEntry) };
+ public static readonly OperationAuthorizationRequirement DeleteProgressEntry =
+ new() { Name = nameof(DeleteProgressEntry) };
+ public static readonly OperationAuthorizationRequirement AddCriticalNote =
+ new() { Name = nameof(AddCriticalNote) };
+ public static readonly OperationAuthorizationRequirement ViewSensitiveRecords =
+ new() { Name = nameof(ViewSensitiveRecords) };
+ public static readonly OperationAuthorizationRequirement GenerateReport =
+ new() { Name = nameof(GenerateReport) };
+}
+```
+
+### 5.3 Resource Objects
+
+Resource objects are lightweight records that carry the context an authorization handler needs to make a decision. They are passed to `AuthorizeAsync` and routed to the appropriate handler by the framework's type-matching system.
+
+```csharp
+public record StudentResource(int StudentId);
+
+public record ProgressEntryResource(int StudentId, int EntryId, int CreatedByUserId);
+
+public record CriticalNoteResource(int StudentId, int NoteId, int CreatedByUserId, string? SensitivityLevel);
+```
+
+### 5.4 Authorization Handlers
+
+#### 5.4.1 Student Authorization Handler
+
+This handler evaluates all student-scoped operations. It loads the user's assignment and checks the `is_primary` flag and user role to determine access.
+
+```csharp
+public class StudentAuthorizationHandler
+ : AuthorizationHandler
+{
+ private readonly IAssignmentRepository _assignments;
+
+ public StudentAuthorizationHandler(IAssignmentRepository assignments)
+ {
+ _assignments = assignments;
+ }
+
+ protected override async Task HandleRequirementAsync(
+ AuthorizationHandlerContext context,
+ OperationAuthorizationRequirement requirement,
+ StudentResource resource)
+ {
+ var userId = context.User.GetUserId();
+ var role = context.User.GetRole();
+
+ var assignment = await _assignments.GetActiveAssignment(userId, resource.StudentId);
+ if (assignment is null)
+ return;
+
+ switch (requirement.Name)
+ {
+ case nameof(Operations.ViewStudent):
+ // Any active assignment grants view access
+ context.Succeed(requirement);
+ break;
+
+ case nameof(Operations.EditStudent):
+ case nameof(Operations.CreateGoal):
+ case nameof(Operations.EditGoal):
+ case nameof(Operations.ArchiveGoal):
+ case nameof(Operations.ViewSensitiveRecords):
+ // Only the primary teacher
+ if (assignment.IsPrimary && role == Role.Teacher)
+ context.Succeed(requirement);
+ break;
+
+ case nameof(Operations.AddProgressEntry):
+ case nameof(Operations.AddCriticalNote):
+ // Teachers and paraeducators with any assignment
+ if (role is Role.Teacher or Role.Paraeducator)
+ context.Succeed(requirement);
+ break;
+
+ case nameof(Operations.GenerateReport):
+ // Teachers and supervisors
+ if (role is Role.Teacher or Role.Supervisor)
+ context.Succeed(requirement);
+ break;
+ }
+ }
+}
+```
+
+#### 5.4.2 Progress Entry Authorization Handler
+
+This handler evaluates entry-level ownership for edit and delete operations. It is called after the student-level check has already passed in the controller.
+
+```csharp
+public class ProgressEntryAuthorizationHandler
+ : AuthorizationHandler
+{
+ private readonly IAssignmentRepository _assignments;
+
+ public ProgressEntryAuthorizationHandler(IAssignmentRepository assignments)
+ {
+ _assignments = assignments;
+ }
+
+ protected override async Task HandleRequirementAsync(
+ AuthorizationHandlerContext context,
+ OperationAuthorizationRequirement requirement,
+ ProgressEntryResource resource)
+ {
+ var userId = context.User.GetUserId();
+ var role = context.User.GetRole();
+
+ switch (requirement.Name)
+ {
+ case nameof(Operations.EditProgressEntry):
+ case nameof(Operations.DeleteProgressEntry):
+ // Any author can edit or delete their own entry
+ if (resource.CreatedByUserId == userId)
+ {
+ context.Succeed(requirement);
+ return;
+ }
+
+ // Primary teachers can edit or delete anyone's entry
+ // for their assigned students
+ if (role == Role.Teacher)
+ {
+ var assignment = await _assignments
+ .GetActiveAssignment(userId, resource.StudentId);
+ if (assignment is not null && assignment.IsPrimary)
+ context.Succeed(requirement);
+ }
+ break;
+ }
+ }
+}
+```
+
+### 5.5 Handler Registration
+
+```csharp
+builder.Services.AddAuthorizationCore();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+```
+
+---
+
+## 6. Data Access Patterns
+
+### 6.1 Single-Resource Endpoints
+
+For endpoints that operate on a specific student, goal, or entry, the controller performs authorization before executing the operation.
+
+```
+Request → Extract resource identifier → AuthorizeAsync → Proceed or return 403
+```
+
+Example flow for updating a goal:
+
+1. Extract `studentId` and `goalId` from the request.
+2. Call `AuthorizeAsync(User, new StudentResource(studentId), Operations.EditGoal)`.
+3. If authorization fails, return `403 Forbidden`.
+4. Load the goal, validate it belongs to the student, and perform the update.
+
+### 6.2 List Endpoints
+
+For endpoints that return collections (e.g., "get my students"), the query joins through `student_assignments` to scope results to the current user. This ensures unauthorized records never leave the database.
+
+```csharp
+public async Task> GetAccessibleStudents(int userId)
+{
+ return await _db.QueryAsync(
+ @"SELECT s.id, s.identifier, s.program_year, s.age,
+ sa.is_primary
+ FROM students s
+ INNER JOIN student_assignments sa ON sa.student_id = s.id
+ WHERE sa.user_id = @UserId
+ AND sa.is_active = TRUE
+ AND sa.start_date <= CURDATE()
+ AND (sa.end_date IS NULL OR sa.end_date >= CURDATE())
+ AND s.is_deleted = FALSE
+ ORDER BY s.identifier",
+ new { UserId = userId });
+}
+```
+
+This query is role-agnostic. Supervisors, teachers, and paraeducators all use the same query. The assignments table determines what each user sees.
+
+### 6.3 Assignment Caching
+
+Because the assignment lookup is called on every single-resource authorization check, the repository is wrapped with a per-request cache to avoid redundant database queries within a single HTTP request.
+
+```csharp
+public class CachedAssignmentRepository : IAssignmentRepository
+{
+ private readonly IAssignmentRepository _inner;
+ private readonly Dictionary<(int, int), StudentAssignment?> _cache = new();
+
+ public CachedAssignmentRepository(IAssignmentRepository inner)
+ {
+ _inner = inner;
+ }
+
+ public async Task GetActiveAssignment(int userId, int studentId)
+ {
+ var key = (userId, studentId);
+ if (_cache.TryGetValue(key, out var cached))
+ return cached;
+
+ var result = await _inner.GetActiveAssignment(userId, studentId);
+ _cache[key] = result;
+ return result;
+ }
+}
+```
+
+This is registered as a scoped service so the cache lives for the duration of one HTTP request.
+
+---
+
+## 7. Core Assignment Query
+
+The core query used by both the authorization handlers and the cached repository:
+
+```sql
+SELECT id, user_id, student_id, is_primary, start_date, end_date, is_active
+FROM student_assignments
+WHERE user_id = @UserId
+ AND student_id = @StudentId
+ AND is_active = TRUE
+ AND start_date <= CURDATE()
+ AND (end_date IS NULL OR end_date >= CURDATE())
+LIMIT 1;
+```
+
+This query enforces both the active flag and the date range, supporting time-bound assignments such as temporary coverage.
+
+---
+
+## 8. Controller Patterns
+
+### 8.1 Single-Resource Authorization
+
+```csharp
+[HttpPut("{goalId}")]
+public async Task UpdateGoal(int studentId, int goalId, [FromBody] UpdateGoalRequest request)
+{
+ var authResult = await _auth.AuthorizeAsync(
+ User, new StudentResource(studentId), Operations.EditGoal);
+ if (!authResult.Succeeded) return Forbid();
+
+ var goal = await _goals.GetById(goalId);
+ if (goal is null || goal.StudentId != studentId) return NotFound();
+
+ await _goals.Update(goalId, request, User.GetUserId());
+ return NoContent();
+}
+```
+
+### 8.2 Two-Layer Authorization (Student + Entry Ownership)
+
+```csharp
+[HttpPut("{entryId}")]
+public async Task UpdateEntry(int studentId, int entryId, [FromBody] UpdateEntryRequest request)
+{
+ // Layer 1: Can you access this student at all?
+ var studentAuth = await _auth.AuthorizeAsync(
+ User, new StudentResource(studentId), Operations.ViewStudent);
+ if (!studentAuth.Succeeded) return Forbid();
+
+ // Load the entry
+ var entry = await _entries.GetById(entryId);
+ if (entry is null || entry.StudentId != studentId) return NotFound();
+
+ // Layer 2: Can you edit THIS entry specifically?
+ var entryAuth = await _auth.AuthorizeAsync(
+ User,
+ new ProgressEntryResource(entry.StudentId, entry.Id, entry.CreatedByUserId),
+ Operations.EditProgressEntry);
+ if (!entryAuth.Succeeded) return Forbid();
+
+ await _entries.Update(entryId, request, User.GetUserId());
+ return NoContent();
+}
+```
+
+### 8.3 List Endpoint (Query-Scoped)
+
+```csharp
+[HttpGet]
+public async Task GetMyStudents()
+{
+ var userId = User.GetUserId();
+ var students = await _students.GetAccessibleStudents(userId);
+ return Ok(students);
+}
+```
+
+---
+
+## 9. Sensitive Record Visibility
+
+Records flagged as sensitive are only visible to users whose assignment has `is_primary = TRUE` and whose role is `Teacher`. This is enforced in two places:
+
+- **Single-resource access**: The `ViewSensitiveRecords` operation is checked via `AuthorizeAsync` before returning sensitive content.
+- **List queries**: Sensitive records are excluded from query results unless the current user meets the primary teacher criteria:
+
+```sql
+AND (pe.is_sensitive = FALSE OR sa.is_primary = TRUE)
+```
+
+---
+
+## 10. Key Design Decisions
+
+### 10.1 Assignments as the Single Source of Truth
+
+All access decisions — whether evaluated in authorization handlers or embedded in SQL queries — derive from the `student_assignments` table. Roles determine the nature of permitted operations. Assignments determine the scope. This separation keeps the system predictable and auditable.
+
+### 10.2 `is_primary` Over Assignment Type Enum
+
+Rather than an enum of assignment types, the `is_primary` boolean provides a clear binary distinction: primary users have full control over a student's goals and records; non-primary users have limited, contributory access. The user's role combined with the `is_primary` flag covers all permission combinations described in the permission matrix.
+
+### 10.3 Supervisors Modeled as Assignments
+
+Supervisors receive explicit assignment records for each student. This avoids special-casing supervisor access in queries and handlers. Supervisors always have `is_primary = FALSE`, which naturally restricts them to read-only operations.
+
+### 10.4 Two Authorization Strategies
+
+Single-resource endpoints use `IAuthorizationService.AuthorizeAsync` with resource objects. List endpoints use query-scoped access via the assignments table. Both strategies use the same underlying assignment data, ensuring consistency.
+
+---
+
+## 11. Testing Strategy
+
+### 11.1 Unit Tests
+
+Each authorization handler should be unit tested in isolation by mocking the assignment repository and asserting `Succeed` or implicit denial for every combination of role, assignment status, `is_primary` flag, and operation.
+
+### 11.2 Integration Tests
+
+List queries should be integration tested to verify that they return only records the user is assigned to. A useful pattern is to compare the results of a list query against individual `AuthorizeAsync` calls for each returned record, asserting that they agree.
+
+### 11.3 Consistency Audit
+
+A periodic or on-demand audit job can iterate over list query results and verify that every returned record passes the corresponding `AuthorizeAsync` check. This catches drift between the two authorization strategies.
\ No newline at end of file
diff --git a/api/src/BaseClasses/BaseController.cs b/api/src/BaseClasses/BaseController.cs
new file mode 100644
index 0000000..dc09d65
--- /dev/null
+++ b/api/src/BaseClasses/BaseController.cs
@@ -0,0 +1,42 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Mvc;
+
+namespace WinStudentGoalTracker.BaseClasses;
+
+public class BaseController : ControllerBase
+{
+ protected (int userId, ActionResult? error) GetUserIdFromClaims()
+ {
+ var userIdClaim = User.FindFirst("user_id")?.Value
+ ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+
+ if (string.IsNullOrWhiteSpace(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
+ {
+ return (0, Unauthorized("Missing or invalid user_id claim."));
+ }
+
+ return (userId, null);
+ }
+
+ protected (string email, List roles, ActionResult? error) GetUserDetailsFromClaims()
+ {
+ var email = User.FindFirst(ClaimTypes.Email)?.Value;
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ return (string.Empty, new List(), Unauthorized("Missing email claim."));
+ }
+
+ var roles = User.FindAll(ClaimTypes.Role).Select(claim => claim.Value).ToList();
+ return (email, roles, null);
+ }
+
+ protected bool HasRole(string role)
+ {
+ return User.IsInRole(role);
+ }
+
+ protected bool HasAnyRole(params string[] roles)
+ {
+ return roles.Any(User.IsInRole);
+ }
+}
diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs
new file mode 100644
index 0000000..57c78f8
--- /dev/null
+++ b/api/src/Controllers/StudentController.cs
@@ -0,0 +1,142 @@
+using Microsoft.AspNetCore.Mvc;
+using WinStudentGoalTracker.Models;
+using WinStudentGoalTracker.BaseClasses;
+using WinStudentGoalTracker.DataAccess;
+
+namespace WinStudentGoalTracker.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class StudentController : BaseController
+{
+ private readonly StudentRepository _studentRepository = new();
+
+
+ // TODO refactor this stored procedure
+ // to getmystudents
+ // This required auth system to be set up first
+ [HttpGet]
+ [ProducesResponseType(typeof(ResponseResult>), StatusCodes.Status200OK)]
+ public async Task>>> GetAll()
+ {
+ var students = await _studentRepository.GetAllAsync();
+ var response = students.Select(StudentResponse.FromDatabaseModel);
+
+ return Ok(new ResponseResult>
+ {
+ Success = true,
+ Data = response
+ });
+ }
+
+ [HttpGet("{idStudent:int}")]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)]
+ public async Task>> GetById(int idStudent)
+ {
+ var student = await _studentRepository.GetByIdAsync(idStudent);
+ if (student is null)
+ {
+ return NotFound(new ResponseResult
+ {
+ Success = false,
+ Message = "Student not found."
+ });
+ }
+
+ return Ok(new ResponseResult
+ {
+ Success = true,
+ Data = StudentResponse.FromDatabaseModel(student)
+ });
+ }
+
+ [HttpPost]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status400BadRequest)]
+ public async Task>> Create([FromBody] CreateStudentDto request)
+ {
+ var existing = await _studentRepository.GetByIdAsync(request.IdStudent);
+ if (existing is not null)
+ {
+ return BadRequest(new ResponseResult
+ {
+ Success = false,
+ Message = $"Student with id {request.IdStudent} already exists."
+ });
+ }
+
+ var created = await _studentRepository.InsertAsync(request);
+ if (created is null)
+ {
+ return BadRequest(new ResponseResult
+ {
+ Success = false,
+ Message = "Unable to create student."
+ });
+ }
+
+ var response = StudentResponse.FromDatabaseModel(created);
+ return CreatedAtAction(nameof(GetById), new { idStudent = response.IdStudent }, new ResponseResult
+ {
+ Success = true,
+ Data = response
+ });
+ }
+
+ [HttpPut("{idStudent:int}")]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)]
+ public async Task>> Update(int idStudent, [FromBody] UpdateStudentDto request)
+ {
+ var existing = await _studentRepository.GetByIdAsync(idStudent);
+ if (existing is null)
+ {
+ return NotFound(new ResponseResult
+ {
+ Success = false,
+ Message = "Student not found."
+ });
+ }
+
+ var updated = await _studentRepository.UpdateAsync(idStudent, request);
+ var refreshed = await _studentRepository.GetByIdAsync(idStudent);
+ if (refreshed is null)
+ {
+ return NotFound(new ResponseResult
+ {
+ Success = false,
+ Message = "Student not found after update."
+ });
+ }
+
+ return Ok(new ResponseResult
+ {
+ Success = true,
+ Message = updated ? null : "No changes were applied.",
+ Data = StudentResponse.FromDatabaseModel(refreshed)
+ });
+ }
+
+ [HttpDelete("{idStudent:int}")]
+ [ProducesResponseType(typeof(ResponseResult