mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 02:57:36 +00:00
Merge branch 'main' of https://github.com/opelly27/WinStudentGoalTracker
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
.env
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>C4G Win</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f5f7fa;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #111827;
|
||||
color: white;
|
||||
padding: 3rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
section {
|
||||
max-width: 900px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>C4G Win</h1>
|
||||
<p>A goal tracking and incident logging platform designed for impact.</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="card">
|
||||
<h2>About the Project</h2>
|
||||
<p>
|
||||
C4G Win is a simple, human-centered goal management system built to help mentors,
|
||||
case managers, and program administrators track progress and document critical incidents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Key Features</h2>
|
||||
<ul>
|
||||
<li>Student goal tracking</li>
|
||||
<li>Progress visualization</li>
|
||||
<li>Incident documentation</li>
|
||||
<li>Secure and privacy-focused design</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Repository</h2>
|
||||
<p>View the source code and contribute on GitHub.</p>
|
||||
<a href="https://github.com/YOUR-USERNAME/YOUR-REPO" class="btn">View on GitHub</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
© 2026 C4G Win Project
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,554 @@
|
||||
# .NET API Project Design Document
|
||||
|
||||
A reference architecture for building ASP.NET Core Web APIs with Dapper, JWT authentication, and a co-located data access layer.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
All code lives in a single project. The data access layer is organized as a namespace within the same assembly rather than a separate project.
|
||||
|
||||
```
|
||||
MyApi/
|
||||
├── MyApi.sln
|
||||
├── MyApi.csproj
|
||||
├── Program.cs
|
||||
├── appsettings.json
|
||||
├── appsettings.Development.json
|
||||
├── Dockerfile
|
||||
├── Configuration/
|
||||
│ └── (Options classes for external services)
|
||||
├── DataAccess/
|
||||
│ ├── DatabaseManager.cs
|
||||
│ ├── Models/
|
||||
│ │ ├── DatabaseObjects/
|
||||
│ │ └── DataTransferObjects/
|
||||
│ └── Repositories/
|
||||
└── src/
|
||||
├── BaseClasses/
|
||||
│ └── BaseController.cs
|
||||
├── Controllers/
|
||||
├── Middleware/
|
||||
├── Models/
|
||||
│ └── ResponseTypes/
|
||||
└── Services/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
### Controllers (`src/Controllers/`)
|
||||
|
||||
Controllers handle HTTP concerns only: request validation, claims extraction, and response shaping. They delegate all business logic to services and all data access to repositories.
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ExampleController : BaseController
|
||||
{
|
||||
private readonly ExampleRepository _exampleRepository = new();
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ResponseResult<ExampleResponse>>> GetById(Guid id)
|
||||
{
|
||||
var (userId, error) = GetUserIdFromClaims();
|
||||
if (error != null) return error;
|
||||
|
||||
var entity = await _exampleRepository.GetByIdAsync(id);
|
||||
if (entity == null) return NotFound();
|
||||
|
||||
return Ok(new ResponseResult<ExampleResponse>
|
||||
{
|
||||
Success = true,
|
||||
Data = MapToResponse(entity)
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
- Use `[ApiController]` and `[Route("api/[controller]")]` on every controller
|
||||
- Inherit from `BaseController` for claims extraction helpers
|
||||
- Return `ActionResult<ResponseResult<T>>` for consistent response envelopes
|
||||
- Apply `[Authorize]` or `[Authorize(Roles = "...")]` per-endpoint
|
||||
- Use `[ProducesResponseType]` attributes for OpenAPI documentation
|
||||
|
||||
### Base Controller (`src/BaseClasses/BaseController.cs`)
|
||||
|
||||
Provides protected helper methods that extract and validate JWT claims, reducing boilerplate across controllers.
|
||||
|
||||
```csharp
|
||||
public class BaseController : ControllerBase
|
||||
{
|
||||
protected (Guid userId, ActionResult? error) GetUserIdFromClaims() { ... }
|
||||
protected (string email, List<string> roles, ActionResult? error) GetUserDetailsFromClaims() { ... }
|
||||
protected bool HasRole(string role) { ... }
|
||||
protected bool HasAnyRole(params string[] roles) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Add additional claim extraction helpers as needed for your domain (e.g., tenant ID, organization ID).
|
||||
|
||||
These methods return tuples with an optional error `ActionResult`, enabling early-return patterns in controller actions.
|
||||
|
||||
### Services (`src/Services/`)
|
||||
|
||||
Services encapsulate business logic that spans multiple repositories or involves non-trivial orchestration. Services are either static classes (for stateless logic) or singletons instantiated directly where needed.
|
||||
|
||||
**When to use a service vs. calling a repository directly from a controller:**
|
||||
- **Direct repository call:** Simple CRUD with no cross-cutting logic
|
||||
- **Service:** Multi-step operations, external API calls, or any logic spanning multiple repositories
|
||||
|
||||
**Static service pattern** (preferred for stateless logic):
|
||||
|
||||
```csharp
|
||||
public static class MyService
|
||||
{
|
||||
public static async Task<Result> DoSomethingAsync(Guid entityId)
|
||||
{
|
||||
var repo = new ExampleRepository();
|
||||
// orchestration logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Singleton instance pattern** (when the service needs initialization or holds config):
|
||||
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
public static MyService Instance { get; private set; } = null!;
|
||||
|
||||
public static void Initialize(string apiKey, string baseUrl)
|
||||
{
|
||||
Instance = new MyService { _apiKey = apiKey, _baseUrl = baseUrl };
|
||||
}
|
||||
|
||||
private string _apiKey;
|
||||
private string _baseUrl;
|
||||
|
||||
public async Task<Result> DoSomethingAsync() { ... }
|
||||
}
|
||||
|
||||
// In Program.cs
|
||||
MyService.Initialize(
|
||||
builder.Configuration["MyService:ApiKey"]!,
|
||||
builder.Configuration["MyService:BaseUrl"]!);
|
||||
```
|
||||
|
||||
### Repositories (`DataAccess/Repositories/`)
|
||||
|
||||
Each repository maps to a single domain entity and serves as a thin C# wrapper around stored procedures. Repositories create their own database connections per call and are responsible for calling stored procedures and assembling the results into typed objects.
|
||||
|
||||
**All query logic lives in stored procedures in the database — repositories contain no inline SQL.**
|
||||
|
||||
```csharp
|
||||
public class ExampleRepository
|
||||
{
|
||||
private IDbConnection Connection =>
|
||||
new MySqlConnection(DatabaseManager.ConnectionString);
|
||||
|
||||
public async Task<dbExample?> GetByIdAsync(Guid id)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QuerySingleOrDefaultAsync<dbExample>(
|
||||
"sp_Example_GetById",
|
||||
new { Id = id },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<dbExample>> GetByOwnerAsync(Guid ownerId)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QueryAsync<dbExample>(
|
||||
"sp_Example_GetByOwner",
|
||||
new { OwnerId = ownerId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> InsertAsync(dbExample entity)
|
||||
{
|
||||
using var db = Connection;
|
||||
var rows = await db.ExecuteAsync(
|
||||
"sp_Example_Insert",
|
||||
new { entity.Id, entity.Name, entity.CreatedAt },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
/// For stored procedures that return multiple result sets,
|
||||
/// use QueryMultipleAsync to assemble a composite object.
|
||||
public async Task<ExampleWithDetails?> GetWithDetailsAsync(Guid id)
|
||||
{
|
||||
using var db = Connection;
|
||||
using var multi = await db.QueryMultipleAsync(
|
||||
"sp_Example_GetWithDetails",
|
||||
new { Id = id },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var example = await multi.ReadSingleOrDefaultAsync<dbExample>();
|
||||
if (example == null) return null;
|
||||
|
||||
var tags = (await multi.ReadAsync<dbTag>()).ToList();
|
||||
var history = (await multi.ReadAsync<dbHistoryEntry>()).ToList();
|
||||
|
||||
return new ExampleWithDetails
|
||||
{
|
||||
Example = example,
|
||||
Tags = tags,
|
||||
History = history
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
- One repository per domain entity
|
||||
- All methods are `async Task<T>`
|
||||
- Use `using var db = Connection;` to ensure connection disposal
|
||||
- **All data access goes through stored procedures** — no inline SQL in repositories
|
||||
- Always pass `commandType: CommandType.StoredProcedure` to Dapper calls
|
||||
- Use `QueryMultipleAsync` when a stored procedure returns multiple result sets, and assemble the results into composite objects
|
||||
- Return `db`-prefixed model types from database operations
|
||||
- Stored procedure naming convention: `sp_{Entity}_{Action}` (e.g., `sp_Example_GetById`, `sp_User_Insert`)
|
||||
|
||||
### Database Objects (`DataAccess/Models/DatabaseObjects/`)
|
||||
|
||||
Plain C# classes that map directly to database tables. Use the `db` prefix to distinguish them from response DTOs.
|
||||
|
||||
```csharp
|
||||
public class dbExample
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
- `db` prefix on all database object class names
|
||||
- Use `required` for non-nullable columns
|
||||
- Use nullable types (`string?`, `Guid?`) for optional columns
|
||||
- Include `CreatedAt` and `UpdatedAt` timestamps on all entities
|
||||
- Use `bool Deleted` for soft-delete support where needed
|
||||
|
||||
### Data Transfer Objects (`DataAccess/Models/DataTransferObjects/`)
|
||||
|
||||
DTOs define the shape of data entering repositories from controllers. They are distinct from both database objects and response types.
|
||||
|
||||
```csharp
|
||||
public class CreateExampleDTO
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateExampleDTO
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Response Types (`src/Models/ResponseTypes/`)
|
||||
|
||||
Response types define the shape of data returned to API consumers. Use a standard envelope.
|
||||
|
||||
```csharp
|
||||
public class ResponseResult<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public T? Data { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Domain-specific response classes map from database objects, excluding internal fields and reshaping data for client consumption.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Configuration Helper
|
||||
|
||||
A static helper class provides access to `IConfiguration` from anywhere in the application, without dependency injection.
|
||||
|
||||
```csharp
|
||||
// Configuration/ConfigHelper.cs
|
||||
public static class ConfigHelper
|
||||
{
|
||||
public static IConfiguration Configuration { get; set; } = null!;
|
||||
}
|
||||
|
||||
// Program.cs (before any services are used)
|
||||
ConfigHelper.Configuration = builder.Configuration;
|
||||
```
|
||||
|
||||
Configuration values are read directly where needed:
|
||||
|
||||
```csharp
|
||||
var apiKey = ConfigHelper.Configuration["MyService:ApiKey"];
|
||||
var baseUrl = ConfigHelper.Configuration["MyService:BaseUrl"];
|
||||
```
|
||||
|
||||
For services that need config at initialization, pass values explicitly during setup in `Program.cs` rather than reading config inside the service.
|
||||
|
||||
### DatabaseManager
|
||||
|
||||
A static class that provides the connection string to all repositories. Configures Dapper's snake_case-to-PascalCase column mapping.
|
||||
|
||||
```csharp
|
||||
public static class DatabaseManager
|
||||
{
|
||||
private static IConfiguration? _configuration;
|
||||
|
||||
private static IConfiguration Configuration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_configuration == null)
|
||||
{
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
_configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
}
|
||||
return _configuration;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConnectionString =>
|
||||
Configuration.GetConnectionString("DefaultConnection")
|
||||
?? throw new MissingFieldException("DefaultConnection not configured");
|
||||
}
|
||||
```
|
||||
|
||||
### appsettings.json Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"Jwt": {
|
||||
"Key": "<base64-encoded-signing-key>",
|
||||
"Issuer": "MyApi"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=...;Database=...;Uid=...;Pwd=...;Pooling=true;Max Pool Size=200;Min Pool Size=5;"
|
||||
},
|
||||
"MyService": { ... },
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Configuration
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Convert.FromBase64String(builder.Configuration["Jwt:Key"]!)),
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Token Service
|
||||
|
||||
A static helper class that generates JWTs with custom claims. Reads JWT config via `ConfigHelper`. Add whatever domain-specific claims your application requires (e.g., user ID, tenant/org ID, email).
|
||||
|
||||
```csharp
|
||||
public static class TokenService
|
||||
{
|
||||
public static string GenerateToken(Guid userId, string email, List<string> roles)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("user_id", userId.ToString()),
|
||||
new(ClaimTypes.Email, email)
|
||||
};
|
||||
// Add domain-specific claims as needed
|
||||
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||
|
||||
var key = new SymmetricSecurityKey(
|
||||
Convert.FromBase64String(ConfigHelper.Configuration["Jwt:Key"]!));
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: ConfigHelper.Configuration["Jwt:Issuer"],
|
||||
expires: DateTime.UtcNow.AddMinutes(15),
|
||||
claims: claims,
|
||||
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Role-Based Authorization
|
||||
|
||||
Define roles as string constants in a static class and apply them via attributes.
|
||||
|
||||
```csharp
|
||||
public static class UserRoles
|
||||
{
|
||||
public const string Admin = "Admin";
|
||||
public const string User = "User";
|
||||
// Add application-specific roles as needed
|
||||
}
|
||||
|
||||
// Usage on endpoints
|
||||
[Authorize(Roles = UserRoles.Admin)]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> Delete(Guid id) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Middleware Pipeline
|
||||
|
||||
Configure middleware in `Program.cs` in the following order:
|
||||
|
||||
```csharp
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseMiddleware<GlobalExceptionHandler>(); // 1. Catch unhandled exceptions
|
||||
app.UseMiddleware<RequestLoggingMiddleware>(); // 2. Log all requests/responses
|
||||
app.UseCors(); // 3. CORS policy
|
||||
app.UseHttpsRedirection(); // 4. HTTPS enforcement
|
||||
app.UseAuthentication(); // 5. JWT validation
|
||||
app.UseAuthorization(); // 6. Role/policy checks
|
||||
app.MapControllers(); // 7. Route to controllers
|
||||
```
|
||||
|
||||
### Request Logging Middleware
|
||||
|
||||
Captures request/response details to the database for observability:
|
||||
- HTTP method, path, query string, status code
|
||||
- Request and response bodies (excluding multipart/form-data)
|
||||
- User ID extracted from JWT claims
|
||||
- Processing time in milliseconds
|
||||
- Request trace ID for correlation
|
||||
|
||||
---
|
||||
|
||||
## Program.cs Template
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// --- Static configuration (must come first) ---
|
||||
ConfigHelper.Configuration = builder.Configuration;
|
||||
|
||||
// --- Kestrel configuration ---
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
|
||||
});
|
||||
|
||||
// --- Authentication ---
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options => { /* see JWT Configuration above */ });
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// --- Controllers ---
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// --- Background services (if needed) ---
|
||||
// builder.Services.AddHostedService<MyBackgroundWorker>();
|
||||
|
||||
// --- CORS ---
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Forwarded headers (for reverse proxy) ---
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
// --- Initialize singleton services ---
|
||||
MyService.Initialize(
|
||||
builder.Configuration["MyService:ApiKey"]!,
|
||||
builder.Configuration["MyService:BaseUrl"]!);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
app.UseMiddleware<GlobalExceptionHandler>();
|
||||
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||
app.UseCors();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Instantiation Patterns
|
||||
|
||||
| Component | Pattern | Example |
|
||||
|---|---|---|
|
||||
| Repositories | `new` in controllers | `private readonly ExampleRepository _repo = new();` |
|
||||
| Stateless services | Static class | `TokenService.GenerateToken(...)` |
|
||||
| Stateful services | Singleton with `Initialize` | `MyService.Instance.DoSomething()` |
|
||||
| Configuration | Static helper | `ConfigHelper.Configuration["Key"]` |
|
||||
| Background workers | Hosted Service (only DI used) | `builder.Services.AddHostedService<MyWorker>()` |
|
||||
|
||||
The only use of the DI container is for framework-level concerns that require it: authentication, authorization, CORS, hosted background services, and forwarded headers. All application-level code uses static classes or direct instantiation.
|
||||
|
||||
---
|
||||
|
||||
## Key Technology Choices
|
||||
|
||||
| Concern | Technology |
|
||||
|---|---|
|
||||
| Framework | ASP.NET Core 9.0 |
|
||||
| ORM | Dapper (micro-ORM, stored procedure calls only) |
|
||||
| Database | MySQL via MySql.Data |
|
||||
| Authentication | JWT Bearer tokens |
|
||||
|
||||
---
|
||||
|
||||
## Patterns & Conventions Summary
|
||||
|
||||
1. **Repository pattern** with one repository per domain entity
|
||||
2. **Stored procedures exclusively** — all query logic lives in the database, repositories are thin wrappers
|
||||
3. **Snake_case DB columns** mapped automatically to PascalCase C# properties
|
||||
4. **Standard response envelope** (`ResponseResult<T>`) on all endpoints
|
||||
5. **Claims-based authorization** with role constants and BaseController helpers
|
||||
6. **Static configuration helper** for accessing `appsettings.json` anywhere
|
||||
7. **No dependency injection** for application code — static classes and direct instantiation only
|
||||
8. **Async/await throughout** — no synchronous database calls
|
||||
9. **Soft deletes** via `Deleted` boolean flag where needed
|
||||
10. **Audit timestamps** (`CreatedAt`, `UpdatedAt`) on all entities
|
||||
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: winstudent-mysql
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3309:3306"
|
||||
volumes:
|
||||
- win_mysql_data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
win_mysql_data:
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="WIN Student Goal Tracker — a student support & case management system for tracking goals, documenting services, and logging critical incidents with auditability and privacy in mind." />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<title>WIN — Student Support & Case Management</title>
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="WIN — Student Support & Case Management" />
|
||||
<meta property="og:description" content="Track goals, services, and critical incidents for students with secure, auditable workflows designed for student support programs." />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body{
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
margin:0;
|
||||
background:#f5f7fa;
|
||||
color:#111827;
|
||||
line-height:1.6;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a{ color: inherit; }
|
||||
a:focus-visible{
|
||||
outline:3px solid #111827;
|
||||
outline-offset:3px;
|
||||
border-radius:8px;
|
||||
}
|
||||
|
||||
.skip-link{
|
||||
position:absolute;
|
||||
left:-999px;
|
||||
top:0;
|
||||
background:#fff;
|
||||
color:#111827;
|
||||
padding:0.75rem 1rem;
|
||||
border-radius:10px;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.12);
|
||||
z-index:1000;
|
||||
}
|
||||
.skip-link:focus{
|
||||
left:1rem;
|
||||
top:1rem;
|
||||
outline:3px solid #111827;
|
||||
outline-offset:2px;
|
||||
}
|
||||
|
||||
header{
|
||||
background:#111827;
|
||||
color:#fff;
|
||||
padding:3.25rem 1.5rem 2.5rem;
|
||||
text-align:center;
|
||||
}
|
||||
header h1{
|
||||
margin:0;
|
||||
font-size:clamp(2rem, 4vw, 2.9rem);
|
||||
letter-spacing:-0.02em;
|
||||
}
|
||||
header p{
|
||||
margin:0.75rem auto 0;
|
||||
max-width:70ch;
|
||||
opacity:0.93;
|
||||
}
|
||||
|
||||
.hero-actions{
|
||||
margin-top:1.25rem;
|
||||
display:flex;
|
||||
gap:0.75rem;
|
||||
justify-content:center;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
main{
|
||||
max-width:980px;
|
||||
margin:2rem auto;
|
||||
padding:0 1.5rem;
|
||||
}
|
||||
|
||||
.grid{
|
||||
display:grid;
|
||||
gap:1.25rem;
|
||||
}
|
||||
@media (min-width: 860px){
|
||||
.grid-2{ grid-template-columns: 1fr 1fr; }
|
||||
.grid-3{ grid-template-columns: 1fr 1fr 1fr; }
|
||||
}
|
||||
|
||||
.card{
|
||||
background:#fff;
|
||||
padding:1.5rem;
|
||||
border-radius:14px;
|
||||
box-shadow:0 4px 10px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
h2{
|
||||
margin:0 0 0.5rem;
|
||||
font-size:1.3rem;
|
||||
}
|
||||
h3{
|
||||
margin:0 0 0.25rem;
|
||||
font-size:1.05rem;
|
||||
}
|
||||
|
||||
ul{
|
||||
margin:0.75rem 0 0;
|
||||
padding-left:1.2rem;
|
||||
}
|
||||
|
||||
.pill-row{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:0.5rem;
|
||||
margin-top:0.75rem;
|
||||
}
|
||||
.pill{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:0.4rem;
|
||||
padding:0.4rem 0.65rem;
|
||||
border-radius:999px;
|
||||
border:1px solid #e5e7eb;
|
||||
background:#f9fafb;
|
||||
font-size:0.92rem;
|
||||
color:#111827;
|
||||
}
|
||||
|
||||
.btn{
|
||||
display:inline-block;
|
||||
padding:0.8rem 1.1rem;
|
||||
background:#1d4ed8;
|
||||
color:#fff;
|
||||
text-decoration:none;
|
||||
border-radius:10px;
|
||||
font-weight:800;
|
||||
border:2px solid transparent;
|
||||
}
|
||||
.btn:hover{ background:#1e40af; }
|
||||
.btn:focus-visible{
|
||||
outline:3px solid #fff;
|
||||
outline-offset:3px;
|
||||
border-color:#111827;
|
||||
}
|
||||
|
||||
.btn.secondary{
|
||||
background:transparent;
|
||||
color:#fff;
|
||||
border-color: rgba(255,255,255,0.55);
|
||||
font-weight:800;
|
||||
}
|
||||
.btn.secondary:hover{
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.muted{ color:#374151; }
|
||||
|
||||
.callout{
|
||||
border-left:6px solid #1d4ed8;
|
||||
padding-left:1rem;
|
||||
background:#ffffff;
|
||||
}
|
||||
|
||||
.kpi{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:0.15rem;
|
||||
padding:1rem;
|
||||
border-radius:14px;
|
||||
background:#ffffff;
|
||||
box-shadow:0 4px 10px rgba(0,0,0,0.06);
|
||||
border:1px solid #eef2ff;
|
||||
}
|
||||
.kpi strong{
|
||||
font-size:1.3rem;
|
||||
letter-spacing:-0.01em;
|
||||
}
|
||||
.kpi span{
|
||||
color:#374151;
|
||||
font-size:0.95rem;
|
||||
}
|
||||
|
||||
footer{
|
||||
text-align:center;
|
||||
padding:2rem 1rem;
|
||||
font-size:0.95rem;
|
||||
color:#374151;
|
||||
}
|
||||
|
||||
code{
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size:0.92em;
|
||||
background:#f3f4f6;
|
||||
padding:0.1rem 0.3rem;
|
||||
border-radius:6px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*{
|
||||
scroll-behavior:auto !important;
|
||||
transition:none !important;
|
||||
animation:none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
|
||||
<header role="banner" aria-label="Project header">
|
||||
<h1>WIN — Student Support & Case Management</h1>
|
||||
<p>
|
||||
A privacy-minded system for tracking student goals, documenting services and notes,
|
||||
and logging critical incidents with clear audit trails and role-based access.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions" role="group" aria-label="Primary actions">
|
||||
<a
|
||||
href="https://github.com/opelly27/WinStudentGoalTracker"
|
||||
class="btn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open the WIN Student Goal Tracker repository on GitHub (opens in a new tab)"
|
||||
>
|
||||
View Repository
|
||||
</a>
|
||||
<a href="#getting-started" class="btn secondary" aria-label="Jump to getting started section">
|
||||
Getting Started
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" role="main">
|
||||
<section class="grid grid-3" aria-label="Project highlights">
|
||||
<div class="kpi" aria-label="Goal tracking highlight">
|
||||
<strong>Goal Tracking</strong>
|
||||
<span>Define goals, categories, status, and progress over time.</span>
|
||||
</div>
|
||||
<div class="kpi" aria-label="Incident logging highlight">
|
||||
<strong>Incident Logging</strong>
|
||||
<span>Capture critical events with consistent workflows and escalation.</span>
|
||||
</div>
|
||||
<div class="kpi" aria-label="Auditability highlight">
|
||||
<strong>Auditability</strong>
|
||||
<span>Record who did what and when for sensitive actions.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-2" style="margin-top:1.25rem;" aria-label="Overview and intended users">
|
||||
<section class="card" aria-labelledby="about">
|
||||
<h2 id="about">What WIN Is</h2>
|
||||
<p class="muted">
|
||||
WIN supports student success programs by providing a single place to manage student profiles,
|
||||
goals, case notes, services provided, and critical incident reports—designed around
|
||||
day-to-day workflows for advisors, mentors, and case managers.
|
||||
</p>
|
||||
<div class="pill-row" aria-label="Core modules">
|
||||
<span class="pill">Students</span>
|
||||
<span class="pill">Goals</span>
|
||||
<span class="pill">Case Notes</span>
|
||||
<span class="pill">Services</span>
|
||||
<span class="pill">Incidents</span>
|
||||
<span class="pill">Reports</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="users">
|
||||
<h2 id="users">Who Uses It</h2>
|
||||
<ul>
|
||||
<li><strong>Case Managers / Advisors:</strong> track goals, document interactions, and follow up.</li>
|
||||
<li><strong>Program Administrators:</strong> monitor outcomes and compliance-ready reporting.</li>
|
||||
<li><strong>Supervisors:</strong> review incidents, approve actions, and ensure accountability.</li>
|
||||
</ul>
|
||||
<p class="muted" style="margin-top:0.75rem;">
|
||||
WIN is built for least-privilege access and consistent documentation.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="card callout" style="margin-top:1.25rem;" aria-labelledby="workflow">
|
||||
<h2 id="workflow">Typical Workflow</h2>
|
||||
<ol style="margin:0.5rem 0 0; padding-left:1.25rem;">
|
||||
<li>Select a student and review current goals and status.</li>
|
||||
<li>Add or update goals (category, status, progress notes).</li>
|
||||
<li>Record case notes and services provided for that session.</li>
|
||||
<li>If needed, log a critical incident with severity and follow-up actions.</li>
|
||||
<li>Generate reports for program outcomes and internal review.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-2" style="margin-top:1.25rem;" aria-label="Capabilities">
|
||||
<section class="card" aria-labelledby="features">
|
||||
<h2 id="features">Core Features</h2>
|
||||
<ul>
|
||||
<li><strong>Student profiles:</strong> basic demographics and program context.</li>
|
||||
<li><strong>Goal management:</strong> categories, statuses, and progress visibility.</li>
|
||||
<li><strong>Case documentation:</strong> notes and service tracking aligned to program needs.</li>
|
||||
<li><strong>Critical incidents:</strong> consistent logging, review, and escalation support.</li>
|
||||
<li><strong>Search & filters:</strong> quickly locate students and prioritize follow-ups.</li>
|
||||
<li><strong>Reporting:</strong> outcomes and activity summaries for administrators.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="compliance">
|
||||
<h2 id="compliance">Privacy & Compliance Design</h2>
|
||||
<p class="muted">
|
||||
WIN is intended for environments where student records require careful handling.
|
||||
The design emphasizes confidentiality, integrity, auditability, and controlled access.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Role-based access control:</strong> permissions enforced server-side.</li>
|
||||
<li><strong>Audit logs:</strong> track create/update/delete for sensitive records.</li>
|
||||
<li><strong>Data minimization:</strong> collect only what’s necessary for the workflow.</li>
|
||||
<li><strong>Secure defaults:</strong> least privilege and predictable access patterns.</li>
|
||||
</ul>
|
||||
<p class="muted" style="margin-top:0.75rem;">
|
||||
See <code>SECURITY.md</code> in the repo for reporting and expectations.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-2" style="margin-top:1.25rem;" aria-label="Engineering">
|
||||
<section class="card" aria-labelledby="architecture">
|
||||
<h2 id="architecture">Project Structure</h2>
|
||||
<ul>
|
||||
<li><code>api/</code> — backend services and data access</li>
|
||||
<li><code>ui/</code> — front-end application</li>
|
||||
<li><code>SECURITY.md</code> — security reporting and practices</li>
|
||||
<li><code>README.md</code> — build/run instructions and overview</li>
|
||||
</ul>
|
||||
<p class="muted" style="margin-top:0.75rem;">
|
||||
This landing page is intended for GitHub Pages and project communication.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card" id="getting-started" aria-labelledby="getting-started-title">
|
||||
<h2 id="getting-started-title">Getting Started</h2>
|
||||
<ol style="margin:0.5rem 0 0; padding-left:1.25rem;">
|
||||
<li>Open the repository and review <code>README.md</code>.</li>
|
||||
<li>Set up the API environment (local config, database connection, secrets).</li>
|
||||
<li>Run the UI and validate core flows: student list → goals → incident logging.</li>
|
||||
</ol>
|
||||
<p class="muted" style="margin-top:0.75rem;">
|
||||
Want to contribute? Open a PR with a clear description, screenshots (if UI),
|
||||
and notes on security/privacy impacts.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/opelly27/WinStudentGoalTracker"
|
||||
class="btn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open repository on GitHub (opens in a new tab)"
|
||||
>
|
||||
Go to GitHub
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top:1.25rem;" aria-labelledby="roadmap">
|
||||
<h2 id="roadmap">Roadmap (High-Level)</h2>
|
||||
<div class="grid grid-3" style="margin-top:0.75rem;">
|
||||
<div class="card" style="box-shadow:none; border:1px solid #e5e7eb;">
|
||||
<h3>MVP</h3>
|
||||
<ul>
|
||||
<li>Student list + profile</li>
|
||||
<li>Goal create/update</li>
|
||||
<li>Incident logging</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card" style="box-shadow:none; border:1px solid #e5e7eb;">
|
||||
<h3>Operations</h3>
|
||||
<ul>
|
||||
<li>Search & filters</li>
|
||||
<li>Reports & exports</li>
|
||||
<li>Supervisor review workflow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card" style="box-shadow:none; border:1px solid #e5e7eb;">
|
||||
<h3>Compliance</h3>
|
||||
<ul>
|
||||
<li>Audit log viewer</li>
|
||||
<li>Retention & access review</li>
|
||||
<li>Stronger incident classification</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted" style="margin-top:0.75rem;">
|
||||
Roadmap items should be validated against requirements and stakeholder workflows.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer role="contentinfo">
|
||||
<small>© 2026 WIN Student Goal Tracker — Built for student support, documentation, and accountability.</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user