mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 01:47:41 +00:00
Latest
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ var dbName = Environment.GetEnvironmentVariable("MYSQL_DATABASE") ?? "winstudent
|
||||
var dbUser = Environment.GetEnvironmentVariable("MYSQL_USER") ?? "root";
|
||||
var dbPassword = Environment.GetEnvironmentVariable("MYSQL_PASSWORD") ?? "";
|
||||
builder.Configuration["ConnectionStrings:DefaultConnection"] =
|
||||
$"Server={dbServer};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};";
|
||||
$"Server={dbServer};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};SslMode=Disabled;";
|
||||
|
||||
// Override JWT key from .env if present
|
||||
var envJwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
|
||||
<PackageReference Include="MySql.Data" Version="8.4.0" />
|
||||
<PackageReference Include="MySql.Data" Version="9.6.0" />
|
||||
<PackageReference Include="MySqlBackup.NET" Version="2.7.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WinStudentGoalTracker.BaseClasses;
|
||||
using WinStudentGoalTracker.DataAccess;
|
||||
using WinStudentGoalTracker.Models;
|
||||
using WinStudentGoalTracker.Services;
|
||||
|
||||
namespace WinStudentGoalTracker.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AdminController : BaseController
|
||||
{
|
||||
private readonly AdminRepository _adminRepo = new();
|
||||
|
||||
// *****************************************************************
|
||||
// Returns the district ID for the current user by resolving the
|
||||
// program→district FK relationship. The JWT carries program_id
|
||||
// but not district_id; this method bridges that gap. This is a
|
||||
// deliberate design choice: the JWT stays lean, and we derive
|
||||
// the district from the program FK on each request.
|
||||
// *****************************************************************
|
||||
private async Task<(Guid districtId, ActionResult? error)> GetDistrictForCurrentUser(Guid programId)
|
||||
{
|
||||
var districtId = await _adminRepo.GetDistrictIdForProgramAsync(programId);
|
||||
if (!districtId.HasValue)
|
||||
{
|
||||
return (Guid.Empty, NotFound(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "District not found for the current program."
|
||||
}));
|
||||
}
|
||||
return (districtId.Value, null);
|
||||
}
|
||||
|
||||
// ************************ Programs *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all programs for the current user's district.
|
||||
// *****************************************************************
|
||||
[HttpGet("programs")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<List<object>>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ResponseResult<List<object>>>> GetPrograms()
|
||||
{
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
var (districtId, districtError) = await GetDistrictForCurrentUser(programId);
|
||||
if (districtError is not null) return districtError;
|
||||
|
||||
var programs = await _adminRepo.GetProgramsByDistrictAsync(districtId);
|
||||
var result = programs.Select(p => new
|
||||
{
|
||||
programId = p.IdProgram,
|
||||
name = p.Name,
|
||||
description = p.Description,
|
||||
createdAt = p.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
return Ok(new ResponseResult<List<object>>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Programs retrieved.",
|
||||
Data = result.Cast<object>().ToList()
|
||||
});
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Creates a new program under the current user's district.
|
||||
// *****************************************************************
|
||||
[HttpPost("programs")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResponseResult<object>>> CreateProgram([FromBody] AdminCreateProgramDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Program name is required."
|
||||
});
|
||||
}
|
||||
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
var (districtId, districtError) = await GetDistrictForCurrentUser(programId);
|
||||
if (districtError is not null) return districtError;
|
||||
|
||||
var newProgramId = Guid.NewGuid();
|
||||
var created = await _adminRepo.CreateProgramAsync(newProgramId, districtId, dto.Name, dto.Description);
|
||||
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Program created.",
|
||||
Data = new
|
||||
{
|
||||
programId = newProgramId,
|
||||
name = dto.Name,
|
||||
description = dto.Description
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Updates a program's name and description.
|
||||
// *****************************************************************
|
||||
[HttpPut("programs/{idProgram:guid}")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ResponseResult<object>>> UpdateProgram(Guid idProgram, [FromBody] AdminCreateProgramDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Program name is required."
|
||||
});
|
||||
}
|
||||
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
// Verify the program belongs to the user's district
|
||||
var (districtId, districtError) = await GetDistrictForCurrentUser(programId);
|
||||
if (districtError is not null) return districtError;
|
||||
|
||||
var targetDistrictId = await _adminRepo.GetDistrictIdForProgramAsync(idProgram);
|
||||
if (!targetDistrictId.HasValue || targetDistrictId.Value != districtId)
|
||||
{
|
||||
return NotFound(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Program not found in your district."
|
||||
});
|
||||
}
|
||||
|
||||
await _adminRepo.UpdateProgramAsync(idProgram, dto.Name, dto.Description);
|
||||
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Program updated."
|
||||
});
|
||||
}
|
||||
|
||||
// ************************ Users *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all active users across programs in the user's district.
|
||||
// *****************************************************************
|
||||
[HttpGet("users")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<List<object>>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ResponseResult<List<object>>>> GetUsers()
|
||||
{
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
var (districtId, districtError) = await GetDistrictForCurrentUser(programId);
|
||||
if (districtError is not null) return districtError;
|
||||
|
||||
var users = await _adminRepo.GetUsersByDistrictAsync(districtId);
|
||||
var result = users.Select(u => new
|
||||
{
|
||||
userId = u.IdUser,
|
||||
email = u.Email,
|
||||
name = u.Name,
|
||||
programId = u.IdProgram,
|
||||
programName = u.ProgramName,
|
||||
roleId = u.IdRole,
|
||||
roleName = u.RoleName,
|
||||
createdAt = u.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
return Ok(new ResponseResult<List<object>>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Users retrieved.",
|
||||
Data = result.Cast<object>().ToList()
|
||||
});
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Creates a new user and assigns them to a program with a role.
|
||||
// District admins cannot assign the super_admin role.
|
||||
// *****************************************************************
|
||||
[HttpPost("users")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResponseResult<object>>> CreateUser([FromBody] AdminCreateUserDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Email) ||
|
||||
string.IsNullOrWhiteSpace(dto.Name) ||
|
||||
string.IsNullOrWhiteSpace(dto.Password) ||
|
||||
string.IsNullOrWhiteSpace(dto.ProgramId) ||
|
||||
string.IsNullOrWhiteSpace(dto.RoleId))
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Email, name, password, program, and role are required."
|
||||
});
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(dto.ProgramId, out var targetProgramId) ||
|
||||
!Guid.TryParse(dto.RoleId, out var roleId))
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Invalid program or role ID."
|
||||
});
|
||||
}
|
||||
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
// Verify the target program belongs to the user's district
|
||||
var (districtId, districtError) = await GetDistrictForCurrentUser(programId);
|
||||
if (districtError is not null) return districtError;
|
||||
|
||||
var targetDistrictId = await _adminRepo.GetDistrictIdForProgramAsync(targetProgramId);
|
||||
if (!targetDistrictId.HasValue || targetDistrictId.Value != districtId)
|
||||
{
|
||||
return NotFound(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Program not found in your district."
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent district_admin from assigning super_admin role
|
||||
var allRoles = await _adminRepo.GetAllRolesAsync();
|
||||
var selectedRole = allRoles.FirstOrDefault(r => r.IdRole == roleId);
|
||||
if (selectedRole == null)
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Invalid role."
|
||||
});
|
||||
}
|
||||
if (selectedRole.InternalName == UserRoles.SuperAdmin && role != UserRoles.SuperAdmin)
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Only super admins can assign the super admin role."
|
||||
});
|
||||
}
|
||||
|
||||
// Check for duplicate email
|
||||
if (await _adminRepo.EmailExistsAsync(dto.Email))
|
||||
{
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "An account with this email already exists."
|
||||
});
|
||||
}
|
||||
|
||||
// Create user
|
||||
var newUserId = Guid.NewGuid();
|
||||
var (hash, salt) = PasswordHasher.HashPassword(dto.Password);
|
||||
await _adminRepo.CreateUserAsync(newUserId, dto.Email, dto.Name, hash, salt);
|
||||
|
||||
// Assign to program with role
|
||||
await _adminRepo.AssignUserToProgramAsync(newUserId, targetProgramId, roleId);
|
||||
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "User created and assigned to program.",
|
||||
Data = new { userId = newUserId, email = dto.Email, name = dto.Name }
|
||||
});
|
||||
}
|
||||
|
||||
// ************************ Roles *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all available roles. Excludes super_admin for
|
||||
// non-super-admin users.
|
||||
// *****************************************************************
|
||||
[HttpGet("roles")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(ResponseResult<List<object>>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ResponseResult<List<object>>>> GetRoles()
|
||||
{
|
||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||
if (error is not null) return error;
|
||||
|
||||
var roles = await _adminRepo.GetAllRolesAsync();
|
||||
|
||||
// District admins cannot see/assign the super_admin role
|
||||
if (role != UserRoles.SuperAdmin)
|
||||
{
|
||||
roles = roles.Where(r => r.InternalName != UserRoles.SuperAdmin);
|
||||
}
|
||||
|
||||
var result = roles.Select(r => new
|
||||
{
|
||||
roleId = r.IdRole,
|
||||
name = r.Name,
|
||||
internalName = r.InternalName,
|
||||
description = r.Description
|
||||
}).ToList();
|
||||
|
||||
return Ok(new ResponseResult<List<object>>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Roles retrieved.",
|
||||
Data = result.Cast<object>().ToList()
|
||||
});
|
||||
}
|
||||
|
||||
// ************************ Backup *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Exports the entire MySQL database as a .sql dump file using
|
||||
// MySqlBackup.NET. Runs in-process with no shell-out; uses a
|
||||
// single-transaction snapshot for InnoDB consistency (no locking).
|
||||
// Restricted to district/super admins.
|
||||
// *****************************************************************
|
||||
[HttpGet("backup")]
|
||||
[Authorize(Roles = $"{UserRoles.DistrictAdmin},{UserRoles.SuperAdmin}")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult BackupDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var conn = new MySql.Data.MySqlClient.MySqlConnection(DatabaseManager.ConnectionString);
|
||||
conn.Open();
|
||||
|
||||
using var cmd = new MySql.Data.MySqlClient.MySqlCommand { Connection = conn };
|
||||
var backup = new MySql.Data.MySqlClient.MySqlBackup(cmd);
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
backup.ExportToMemoryStream(memoryStream);
|
||||
|
||||
var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
|
||||
var fileName = $"winstudentgoaltracker_backup_{timestamp}.sql";
|
||||
|
||||
return File(memoryStream.ToArray(), "application/sql", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Backup failed: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,4 +452,76 @@ public class AuthController : BaseController
|
||||
Message = "Password set successfully."
|
||||
});
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Self-service registration: creates a school district, the user's
|
||||
// first program, and the user account. The user is assigned as
|
||||
// district_admin on the new program, giving them a program_id for
|
||||
// their JWT and access to create more programs from the admin panel.
|
||||
// *****************************************************************
|
||||
[HttpPost("Register")]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResponseResult<object>>> Register([FromBody] RegisterDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Email) ||
|
||||
string.IsNullOrWhiteSpace(dto.Password) ||
|
||||
string.IsNullOrWhiteSpace(dto.Name) ||
|
||||
string.IsNullOrWhiteSpace(dto.DistrictName) ||
|
||||
string.IsNullOrWhiteSpace(dto.ProgramName))
|
||||
{
|
||||
return BadRequest(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "Email, password, name, district name, and program name are required."
|
||||
});
|
||||
}
|
||||
|
||||
var adminRepo = new AdminRepository();
|
||||
|
||||
// Check for duplicate email
|
||||
if (await adminRepo.EmailExistsAsync(dto.Email))
|
||||
{
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "An account with this email already exists."
|
||||
});
|
||||
}
|
||||
|
||||
// Look up the district_admin role
|
||||
var districtAdminRole = await adminRepo.GetRoleByInternalNameAsync(UserRoles.DistrictAdmin);
|
||||
if (districtAdminRole == null)
|
||||
{
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = false,
|
||||
Message = "System configuration error: district_admin role not found."
|
||||
});
|
||||
}
|
||||
|
||||
// Create the school district
|
||||
var districtId = Guid.NewGuid();
|
||||
await adminRepo.CreateDistrictAsync(districtId, dto.DistrictName, dto.DistrictContactEmail);
|
||||
|
||||
// Create the user with hashed password
|
||||
var userId = Guid.NewGuid();
|
||||
var (hash, salt) = PasswordHasher.HashPassword(dto.Password);
|
||||
await adminRepo.CreateUserAsync(userId, dto.Email, dto.Name, hash, salt);
|
||||
|
||||
// Create the user's first program under the new district.
|
||||
// This gives the district_admin a program_id for their JWT,
|
||||
// enabling them to log in and manage the district from the admin panel.
|
||||
var programId = Guid.NewGuid();
|
||||
await adminRepo.CreateProgramAsync(programId, districtId, dto.ProgramName, dto.ProgramDescription);
|
||||
|
||||
// Assign the user as district_admin on the new program
|
||||
await adminRepo.AssignUserToProgramAsync(userId, programId, districtAdminRole.IdRole, isPrimary: true);
|
||||
|
||||
return Ok(new ResponseResult<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "Registration successful. You can now log in."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class AdminCreateProgramDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class AdminCreateUserDto
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? ProgramId { get; set; }
|
||||
public string? RoleId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class RegisterDto
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? DistrictName { get; set; }
|
||||
public string? DistrictContactEmail { get; set; }
|
||||
public string? ProgramName { get; set; }
|
||||
public string? ProgramDescription { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class dbDistrictUserRow
|
||||
{
|
||||
public Guid IdUser { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid IdProgram { get; set; }
|
||||
public string? ProgramName { get; set; }
|
||||
public Guid IdRole { get; set; }
|
||||
public string? RoleName { get; set; }
|
||||
public string? RoleInternalName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class dbProgramRow
|
||||
{
|
||||
public Guid IdProgram { get; set; }
|
||||
public Guid IdSchoolDistrict { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class dbRoleRow
|
||||
{
|
||||
public Guid IdRole { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? InternalName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MySql.Data.MySqlClient;
|
||||
|
||||
namespace WinStudentGoalTracker.DataAccess;
|
||||
|
||||
public class AdminRepository
|
||||
{
|
||||
private IDbConnection Connection => new MySqlConnection(DatabaseManager.ConnectionString);
|
||||
|
||||
// *****************************************************************
|
||||
// Returns the district ID for a given program ID. Used to resolve
|
||||
// the user's district from their JWT program_id claim, since the
|
||||
// JWT does not carry district_id directly. The program→district
|
||||
// FK relationship is the source of truth.
|
||||
// *****************************************************************
|
||||
public async Task<Guid?> GetDistrictIdForProgramAsync(Guid programId)
|
||||
{
|
||||
using var db = Connection;
|
||||
var row = await db.QuerySingleOrDefaultAsync<dbProgramRow>(
|
||||
"sp_Program_GetById",
|
||||
new { p_id_program = programId.ToString() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return row?.IdSchoolDistrict;
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Creates a new school district and returns the created row.
|
||||
// *****************************************************************
|
||||
public async Task<dynamic?> CreateDistrictAsync(Guid districtId, string name, string? contactEmail)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QuerySingleOrDefaultAsync<dynamic>(
|
||||
"sp_SchoolDistrict_Insert",
|
||||
new
|
||||
{
|
||||
p_id_school_district = districtId.ToString(),
|
||||
p_name = name,
|
||||
p_contact_email = contactEmail
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Creates a new user and returns the created row.
|
||||
// *****************************************************************
|
||||
public async Task<dynamic?> CreateUserAsync(Guid userId, string email, string name, string passwordHash, string passwordSalt)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QuerySingleOrDefaultAsync<dynamic>(
|
||||
"sp_User_Insert",
|
||||
new
|
||||
{
|
||||
p_id_user = userId.ToString(),
|
||||
p_email = email,
|
||||
p_name = name,
|
||||
p_password_hash = passwordHash,
|
||||
p_password_salt = passwordSalt
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Assigns a user to a program with a given role.
|
||||
// *****************************************************************
|
||||
public async Task<bool> AssignUserToProgramAsync(Guid userId, Guid programId, Guid roleId, bool isPrimary = true)
|
||||
{
|
||||
using var db = Connection;
|
||||
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||
"sp_UserProgram_Insert",
|
||||
new
|
||||
{
|
||||
p_id_user_program = Guid.NewGuid().ToString(),
|
||||
p_id_user = userId.ToString(),
|
||||
p_id_program = programId.ToString(),
|
||||
p_id_role = roleId.ToString(),
|
||||
p_is_primary = isPrimary ? 1 : 0
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all programs belonging to a given district.
|
||||
// *****************************************************************
|
||||
public async Task<IEnumerable<dbProgramRow>> GetProgramsByDistrictAsync(Guid districtId)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QueryAsync<dbProgramRow>(
|
||||
"sp_Program_GetByDistrictId",
|
||||
new { p_id_school_district = districtId.ToString() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Creates a new program under a given district.
|
||||
// *****************************************************************
|
||||
public async Task<dbProgramRow?> CreateProgramAsync(Guid programId, Guid districtId, string name, string? description)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QuerySingleOrDefaultAsync<dbProgramRow>(
|
||||
"sp_Program_Insert",
|
||||
new
|
||||
{
|
||||
p_id_program = programId.ToString(),
|
||||
p_id_school_district = districtId.ToString(),
|
||||
p_name = name,
|
||||
p_description = description
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Updates an existing program's name and description.
|
||||
// *****************************************************************
|
||||
public async Task<bool> UpdateProgramAsync(Guid programId, string name, string? description)
|
||||
{
|
||||
using var db = Connection;
|
||||
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||
"sp_Program_Update",
|
||||
new
|
||||
{
|
||||
p_id_program = programId.ToString(),
|
||||
p_name = name,
|
||||
p_description = description
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all active users across all programs in a district.
|
||||
// *****************************************************************
|
||||
public async Task<IEnumerable<dbDistrictUserRow>> GetUsersByDistrictAsync(Guid districtId)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QueryAsync<dbDistrictUserRow>(
|
||||
"sp_User_GetByDistrictId",
|
||||
new { p_id_school_district = districtId.ToString() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns all available roles.
|
||||
// *****************************************************************
|
||||
public async Task<IEnumerable<dbRoleRow>> GetAllRolesAsync()
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QueryAsync<dbRoleRow>(
|
||||
"sp_Role_GetAll",
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns a role by its internal_name (e.g. "district_admin").
|
||||
// Queries the role table directly since no SP exists for this.
|
||||
// *****************************************************************
|
||||
public async Task<dbRoleRow?> GetRoleByInternalNameAsync(string internalName)
|
||||
{
|
||||
using var db = Connection;
|
||||
return await db.QuerySingleOrDefaultAsync<dbRoleRow>(
|
||||
"SELECT id_role AS IdRole, name AS Name, internal_name AS InternalName, description AS Description FROM role WHERE internal_name = @internalName",
|
||||
new { internalName });
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Checks if a user with the given email already exists.
|
||||
// *****************************************************************
|
||||
public async Task<bool> EmailExistsAsync(string email)
|
||||
{
|
||||
using var db = Connection;
|
||||
var count = await db.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM `user` WHERE email = @email",
|
||||
new { email });
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user