mirror of
https://github.com/Kizuren/QuizConnect.git
synced 2025-12-21 21:16:14 +01:00
Initial commit
This commit is contained in:
commit
ea82926500
58 changed files with 9323 additions and 0 deletions
25
Server/.dockerignore
Normal file
25
Server/.dockerignore
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
22
Server/Dockerfile
Normal file
22
Server/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 9500
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Server/Server.csproj", "Server/"]
|
||||
RUN dotnet restore "Server/Server.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Server"
|
||||
RUN dotnet build "Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Server.dll"]
|
||||
21
Server/Server.sln
Normal file
21
Server/Server.sln
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{89ED2F06-A378-47E5-8DF8-A44335C01BBA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5BEF36F-A30C-4690-BE85-5B97D83ED7F2}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
compose.yaml = compose.yaml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{89ED2F06-A378-47E5-8DF8-A44335C01BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{89ED2F06-A378-47E5-8DF8-A44335C01BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{89ED2F06-A378-47E5-8DF8-A44335C01BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{89ED2F06-A378-47E5-8DF8-A44335C01BBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
3
Server/Server/.env.example
Normal file
3
Server/Server/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
MONGODB_URI=mongodb://user:password@localhost:27017/database
|
||||
ADMIN_LOGIN_ID=1B3z
|
||||
DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
|
||||
479
Server/Server/Controllers/AdminController.cs
Normal file
479
Server/Server/Controllers/AdminController.cs
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using WritingServer.Models;
|
||||
using WritingServer.Services;
|
||||
using WritingServer.Utils;
|
||||
|
||||
namespace WritingServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IUserManagementService _userManagementService;
|
||||
private readonly IAdminTokenService _tokenService;
|
||||
private readonly IQuestionManagementService _questionManagementService;
|
||||
|
||||
public AdminController(IConfiguration configuration, IUserManagementService userManagementService,
|
||||
IAdminTokenService tokenService, IQuestionManagementService questionManagementService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_userManagementService = userManagementService;
|
||||
_tokenService = tokenService;
|
||||
_questionManagementService = questionManagementService;
|
||||
}
|
||||
|
||||
[HttpGet("test-exception")]
|
||||
public IActionResult TestException()
|
||||
{
|
||||
throw new Exception("This is a test exception");
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public ActionResult<AdminLoginResponse> Login([FromBody] AdminLoginRequest request)
|
||||
{
|
||||
var adminLoginId = _configuration["ADMIN_LOGIN_ID"];
|
||||
|
||||
if (string.IsNullOrEmpty(adminLoginId) || request.LoginId != adminLoginId)
|
||||
{
|
||||
return Ok(new AdminLoginResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid login credentials"
|
||||
});
|
||||
}
|
||||
|
||||
var token = TokenGenerator.GenerateToken(32);
|
||||
_tokenService.StoreToken(token);
|
||||
|
||||
return Ok(new AdminLoginResponse
|
||||
{
|
||||
Success = true,
|
||||
AccessToken = token
|
||||
});
|
||||
}
|
||||
|
||||
private string? GetBearerToken()
|
||||
{
|
||||
if (!HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string auth = authHeader.ToString();
|
||||
if (!auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return auth.Substring(7).Trim();
|
||||
}
|
||||
|
||||
private bool ValidateAdminToken(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _tokenService.ValidateToken(token);
|
||||
}
|
||||
|
||||
#region User Management
|
||||
[HttpGet("users")]
|
||||
public async Task<ActionResult<AdminUserListResponse>> GetUsers()
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminUserListResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var users = await _userManagementService.GetAllUsersAsync();
|
||||
return Ok(new AdminUserListResponse { Success = true, Users = users });
|
||||
}
|
||||
|
||||
[HttpPost("users")]
|
||||
public async Task<ActionResult<AdminUserResponse>> AddUser([FromBody] AdminAddUserRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.UserName))
|
||||
{
|
||||
return BadRequest(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Username cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
await _userManagementService.AddUserAsync(request.UserName);
|
||||
return Ok(new AdminUserResponse { Success = true });
|
||||
}
|
||||
|
||||
[HttpPut("users")]
|
||||
public async Task<ActionResult<AdminUserResponse>> UpdateUser([FromBody] AdminEditUserRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.NewUserName))
|
||||
{
|
||||
return BadRequest(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Username cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _userManagementService.UpdateUserNameAsync(request.UserName, request.NewUserName);
|
||||
|
||||
return Ok(new AdminUserResponse
|
||||
{
|
||||
Success = success,
|
||||
ErrorMessage = success ? null : "Failed to update user - username may already exist"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("users/reset")]
|
||||
public async Task<ActionResult<AdminUserResponse>> ResetUser([FromBody] AdminUserResetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByNameAsync(request.UserName);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "User not found"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _userManagementService.ResetUserAsync(request.UserName);
|
||||
return Ok(new AdminUserResponse { Success = success });
|
||||
}
|
||||
|
||||
[HttpDelete("users")]
|
||||
public async Task<ActionResult<AdminUserResponse>> DeleteUser([FromBody] AdminDeleteUserRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminUserResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _userManagementService.DeleteUserAsync(request.UserName);
|
||||
|
||||
return Ok(new AdminUserResponse
|
||||
{
|
||||
Success = success,
|
||||
ErrorMessage = success ? null : "User not found"
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Question Set Management
|
||||
[HttpGet("questionsets")]
|
||||
public async Task<ActionResult<AdminListQuestionSetsResponse>> GetQuestionSets()
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminListQuestionSetsResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSets = await _questionManagementService.GetAllQuestionSetsAsync();
|
||||
return Ok(new AdminListQuestionSetsResponse { Success = true, QuestionSets = questionSets });
|
||||
}
|
||||
|
||||
[HttpPost("questionsets")]
|
||||
public async Task<ActionResult<AdminQuestionSetResponse>> CreateQuestionSet([FromBody] AdminCreateQuestionSetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.QuestionSetName))
|
||||
{
|
||||
return BadRequest(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question set name cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSet = await _questionManagementService.CreateQuestionSetAsync(request.QuestionSetName);
|
||||
return Ok(new AdminQuestionSetResponse { Success = true, QuestionSet = questionSet });
|
||||
}
|
||||
|
||||
[HttpPut("questionsets/order")]
|
||||
public async Task<ActionResult<AdminQuestionSetResponse>> UpdateQuestionSetOrder([FromBody] AdminUpdateOrderQuestionSetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _questionManagementService.UpdateQuestionSetOrderAsync(request.QuestionSetId, request.QuestionSetOrder);
|
||||
if (!success)
|
||||
{
|
||||
return NotFound(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question set not found"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSet = await _questionManagementService.GetQuestionSetByIdAsync(request.QuestionSetId);
|
||||
return Ok(new AdminQuestionSetResponse { Success = true, QuestionSet = questionSet });
|
||||
}
|
||||
|
||||
[HttpPut("questionsets/lock")]
|
||||
public async Task<ActionResult<AdminQuestionSetResponse>> UpdateQuestionSetLock([FromBody] AdminUpdateLockQuestionSetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _questionManagementService.UpdateQuestionSetLockStatusAsync(request.QuestionSetId, request.Locked);
|
||||
if (!success)
|
||||
{
|
||||
return NotFound(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question set not found"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSet = await _questionManagementService.GetQuestionSetByIdAsync(request.QuestionSetId);
|
||||
return Ok(new AdminQuestionSetResponse { Success = true, QuestionSet = questionSet });
|
||||
}
|
||||
|
||||
[HttpPut("questionsets/name")]
|
||||
public async Task<ActionResult<AdminQuestionSetResponse>> UpdateQuestionSetName([FromBody] AdminUpdateNameQuestionSetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewQuestionSetName))
|
||||
{
|
||||
return BadRequest(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question set name cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _questionManagementService.UpdateQuestionSetNameAsync(request.QuestionSetId, request.NewQuestionSetName);
|
||||
if (!success)
|
||||
{
|
||||
return NotFound(new AdminQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question set not found"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSet = await _questionManagementService.GetQuestionSetByIdAsync(request.QuestionSetId);
|
||||
return Ok(new AdminQuestionSetResponse { Success = true, QuestionSet = questionSet });
|
||||
}
|
||||
|
||||
[HttpDelete("questionsets")]
|
||||
public async Task<ActionResult<AdminDeleteQuestionSetResponse>> DeleteQuestionSet([FromBody] AdminDeleteQuestionSetRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminDeleteQuestionSetResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _questionManagementService.DeleteQuestionSetAsync(request.QuestionSetId);
|
||||
return Ok(new AdminDeleteQuestionSetResponse { Success = success });
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Question Management
|
||||
[HttpGet("questions")]
|
||||
public async Task<ActionResult<AdminListQuestionsResponse>> GetQuestions([FromQuery] string questionSetId)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminListQuestionsResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var questions = await _questionManagementService.GetQuestionsInSetAsync(questionSetId);
|
||||
return Ok(new AdminListQuestionsResponse { Success = true, Questions = questions });
|
||||
}
|
||||
|
||||
[HttpPost("questions")]
|
||||
public async Task<ActionResult<AdminQuestionResponse>> CreateQuestion([FromBody] AdminCreateQuestionRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.QuestionText))
|
||||
{
|
||||
return BadRequest(new AdminQuestionResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question text cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var question = await _questionManagementService.CreateQuestionAsync(
|
||||
request.QuestionSetId,
|
||||
request.QuestionText,
|
||||
request.ExpectedResultText,
|
||||
request.QuestionOrder,
|
||||
request.MinWordLength,
|
||||
request.MaxWordLength);
|
||||
|
||||
return Ok(new AdminQuestionResponse { Success = true, QuestionModels = question });
|
||||
}
|
||||
|
||||
[HttpPut("questions")]
|
||||
public async Task<ActionResult<AdminQuestionResponse>> UpdateQuestion([FromBody] AdminUpdateQuestionRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminQuestionResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.QuestionText))
|
||||
{
|
||||
return BadRequest(new AdminQuestionResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Question text cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var question = await _questionManagementService.UpdateQuestionAsync(
|
||||
request.QuestionId,
|
||||
request.QuestionText,
|
||||
request.ExpectedResultText,
|
||||
request.QuestionOrder,
|
||||
request.MinWordLength,
|
||||
request.MaxWordLength);
|
||||
|
||||
return Ok(new AdminQuestionResponse { Success = true, QuestionModels = question });
|
||||
}
|
||||
|
||||
[HttpDelete("questions")]
|
||||
public async Task<ActionResult<AdminDeleteQuestionResponse>> DeleteQuestion([FromBody] AdminDeleteQuestionRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminDeleteQuestionResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var success = await _questionManagementService.DeleteQuestionAsync(request.QuestionId);
|
||||
return Ok(new AdminDeleteQuestionResponse { Success = success });
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Response Management
|
||||
[HttpGet("responses")]
|
||||
public async Task<ActionResult<AdminGetResponsesResponse>> GetResponses([FromQuery] string questionId)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (!ValidateAdminToken(token))
|
||||
{
|
||||
return Unauthorized(new AdminGetResponsesResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var responses = await _questionManagementService.GetResponsesForQuestionAsync(questionId);
|
||||
return Ok(new AdminGetResponsesResponse { Success = true, Responses = responses });
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
283
Server/Server/Controllers/UserController.cs
Normal file
283
Server/Server/Controllers/UserController.cs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using WritingServer.Models;
|
||||
using WritingServer.Services;
|
||||
|
||||
namespace WritingServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/user")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly IUserManagementService _userManagementService;
|
||||
private readonly IQuestionManagementService _questionManagementService;
|
||||
|
||||
public UserController(IUserManagementService userManagementService, IQuestionManagementService questionManagementService)
|
||||
{
|
||||
_userManagementService = userManagementService;
|
||||
_questionManagementService = questionManagementService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<UserLoginResponse>> Login([FromBody] UserLoginRequest request)
|
||||
{
|
||||
var user = await _userManagementService.GetUserByPinAsync(request.Pin);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Ok(new UserLoginResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid PIN"
|
||||
});
|
||||
}
|
||||
|
||||
var accessToken = _userManagementService.GenerateAccessToken(user.Username);
|
||||
if (user.ResetState)
|
||||
{
|
||||
_userManagementService.ResetLoginState(user.Username);
|
||||
}
|
||||
|
||||
return Ok(new UserLoginResponse
|
||||
{
|
||||
Success = true,
|
||||
AccessToken = accessToken
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("username")]
|
||||
public async Task<ActionResult<UserGetUsernameResponse>> GetUsername()
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetUsernameResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserGetUsernameResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new UserGetUsernameResponse
|
||||
{
|
||||
Success = true,
|
||||
Username = user.Username
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("state")]
|
||||
public async Task<ActionResult<UserGetStateResponse>> GetState()
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new UserGetStateResponse
|
||||
{
|
||||
Success = true,
|
||||
ResetState = user.ResetState
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("questionsets")]
|
||||
public async Task<ActionResult<UserGetQuestionSetsResponse>> GetQuestionSets()
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserGetQuestionSetsResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSets = await _questionManagementService.GetAllQuestionSetsAsync();
|
||||
|
||||
// Only return unlocked question sets to users
|
||||
questionSets.QuestionSets = questionSets.QuestionSets.Where(qs => !qs.Locked).ToList();
|
||||
|
||||
return Ok(new UserGetQuestionSetsResponse
|
||||
{
|
||||
Success = true,
|
||||
QuestionSets = questionSets
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("questions")]
|
||||
public async Task<ActionResult<UserGetQuestionsResponse>> GetQuestions([FromQuery] string questionSetId)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserGetQuestionsResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var questionSet = await _questionManagementService.GetQuestionSetByIdAsync(questionSetId);
|
||||
if (questionSet.Locked)
|
||||
{
|
||||
return BadRequest(new UserGetQuestionsResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "This question set is locked"
|
||||
});
|
||||
}
|
||||
|
||||
var questions = await _questionManagementService.GetQuestionsInSetAsync(questionSetId);
|
||||
return Ok(new UserGetQuestionsResponse
|
||||
{
|
||||
Success = true,
|
||||
Questions = questions
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("responses")]
|
||||
public async Task<ActionResult<UserSubmitResponseResponse>> SubmitResponse([FromBody] UserSubmitResponseRequest request)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserSubmitResponseResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ResponseText))
|
||||
{
|
||||
return BadRequest(new UserSubmitResponseResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Response text cannot be empty"
|
||||
});
|
||||
}
|
||||
|
||||
var question = await _questionManagementService.GetQuestionByIdAsync(request.QuestionId);
|
||||
|
||||
var wordCount = request.ResponseText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
if (wordCount < question.MinWordLength || wordCount > question.MaxWordLength)
|
||||
{
|
||||
return BadRequest(new UserSubmitResponseResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Response must be between {question.MinWordLength} and {question.MaxWordLength} words"
|
||||
});
|
||||
}
|
||||
|
||||
await _questionManagementService.SubmitResponseAsync(request.QuestionId, user.Username, request.ResponseText);
|
||||
return Ok(new UserSubmitResponseResponse { Success = true });
|
||||
}
|
||||
|
||||
[HttpGet("responses")]
|
||||
public async Task<ActionResult<UserGetResponsesResponse>> GetResponses([FromQuery] string questionId)
|
||||
{
|
||||
string? token = GetBearerToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return Unauthorized(new UserGetStateResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Missing or invalid authorization token"
|
||||
});
|
||||
}
|
||||
|
||||
var user = await _userManagementService.GetUserByTokenAsync(token);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized(new UserGetResponsesResponse
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Invalid access token"
|
||||
});
|
||||
}
|
||||
|
||||
var responses = await _questionManagementService.GetResponsesForUserAsync(user.Username, questionId);
|
||||
return Ok(new UserGetResponsesResponse
|
||||
{
|
||||
Success = true,
|
||||
Responses = responses
|
||||
});
|
||||
}
|
||||
|
||||
private string? GetBearerToken()
|
||||
{
|
||||
if (!HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string auth = authHeader.ToString();
|
||||
if (!auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return auth.Substring(7).Trim();
|
||||
}
|
||||
}
|
||||
191
Server/Server/Models/AdminApiModels.cs
Normal file
191
Server/Server/Models/AdminApiModels.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
namespace WritingServer.Models;
|
||||
|
||||
#region Admin Login
|
||||
public class AdminLoginRequest
|
||||
{
|
||||
public string LoginId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminLoginResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region User Administration
|
||||
public class AdminAddUserRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminEditUserRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string NewUserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminDeleteUserRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminUserResetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminUserResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AdminUserListRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminUserListResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public UserModelList Users { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Question Set Management
|
||||
public class AdminCreateQuestionSetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminUpdateOrderQuestionSetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
public int QuestionSetOrder { get; set; }
|
||||
}
|
||||
|
||||
public class AdminUpdateLockQuestionSetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
public bool Locked { get; set; }
|
||||
}
|
||||
|
||||
public class AdminUpdateNameQuestionSetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
public string NewQuestionSetName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminDeleteQuestionSetRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminQuestionSetResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionSet QuestionSet { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AdminDeleteQuestionSetResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AdminListQuestionSetsRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminListQuestionSetsResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionSetList QuestionSets { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Question Management
|
||||
public class AdminCreateQuestionRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
public string QuestionText { get; set; } = string.Empty;
|
||||
public string ExpectedResultText { get; set; } = string.Empty;
|
||||
public int QuestionOrder { get; set; }
|
||||
public int MinWordLength { get; set; }
|
||||
public int MaxWordLength { get; set; }
|
||||
}
|
||||
|
||||
public class AdminUpdateQuestionRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
public string QuestionText { get; set; } = string.Empty;
|
||||
public string ExpectedResultText { get; set; } = string.Empty;
|
||||
public int QuestionOrder { get; set; }
|
||||
public int MinWordLength { get; set; }
|
||||
public int MaxWordLength { get; set; }
|
||||
}
|
||||
|
||||
public class AdminDeleteQuestionRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminQuestionResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionModels QuestionModels { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AdminDeleteQuestionResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AdminListQuestionsRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminListQuestionsResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionModelList Questions { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Reponses
|
||||
public class AdminGetResponsesRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminGetResponsesResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public ResponseList Responses { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
54
Server/Server/Models/QuestionModels.cs
Normal file
54
Server/Server/Models/QuestionModels.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace WritingServer.Models;
|
||||
|
||||
public class QuestionSet
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
public string QuestionSetName { get; set; } = string.Empty;
|
||||
public int QuestionSetOrder { get; set; } = 0;
|
||||
public bool Locked { get; set; } = true;
|
||||
public QuestionModels Questions { get; set; } = new();
|
||||
}
|
||||
|
||||
public class QuestionSetList
|
||||
{
|
||||
public List<QuestionSet> QuestionSets { get; set; } = [];
|
||||
}
|
||||
|
||||
public class QuestionModels
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
public string QuestionText { get; set; } = string.Empty;
|
||||
public string ExpectedResultText { get; set; } = string.Empty;
|
||||
public int QuestionOrder { get; set; } = 0;
|
||||
public int MinWordLength { get; set; }
|
||||
public int MaxWordLength { get; set; }
|
||||
public ResponseList ResponseList { get; set; } = new();
|
||||
}
|
||||
|
||||
public class QuestionModelList
|
||||
{
|
||||
public List<QuestionModels> Questions { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ResponseModels
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.String)]
|
||||
public string ResponseId { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
public string ResponseText { get; set; } = string.Empty;
|
||||
public DateTime ResponseTime { get; set; }
|
||||
}
|
||||
|
||||
public class ResponseList
|
||||
{
|
||||
public List<ResponseModels> Response { get; set; } = [];
|
||||
}
|
||||
94
Server/Server/Models/UserApiModels.cs
Normal file
94
Server/Server/Models/UserApiModels.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
namespace WritingServer.Models;
|
||||
|
||||
#region User Login
|
||||
public class UserLoginRequest
|
||||
{
|
||||
public string Pin { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserLoginResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region User States
|
||||
public class UserGetUsernameRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserGetUsernameResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class UserGetResetState
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserGetStateResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public bool ResetState { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Questions
|
||||
public class UserGetQuestionSetsRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserGetQuestionSetsResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionSetList QuestionSets { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class UserGetQuestionsRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionSetId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserGetQuestionsResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public QuestionModelList Questions { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class UserSubmitResponseRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
public string ResponseText { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserSubmitResponseResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class UserGetResponsesRequest
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class UserGetResponsesResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public ResponseList Responses { get; set; } = new();
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
#endregion
|
||||
21
Server/Server/Models/UserModels.cs
Normal file
21
Server/Server/Models/UserModels.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace WritingServer.Models;
|
||||
|
||||
public class UserModel
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
[JsonIgnore]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Pin { get; set; } = string.Empty;
|
||||
public bool ResetState { get; set; } = false;
|
||||
}
|
||||
|
||||
public class UserModelList
|
||||
{
|
||||
public List<UserModel> Users { get; set; } = [];
|
||||
}
|
||||
87
Server/Server/Program.cs
Normal file
87
Server/Server/Program.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System.Diagnostics;
|
||||
using MongoDB.Driver;
|
||||
using WritingServer.Services;
|
||||
using WritingServer.Utils;
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Load .env file
|
||||
DotNetEnv.Env.Load();
|
||||
|
||||
// Add services to the container
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
// Add CORS configuration
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowClientApp", policy =>
|
||||
{
|
||||
policy.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
|
||||
// Add MongoDB client
|
||||
builder.Services.AddSingleton<IMongoClient>(sp =>
|
||||
{
|
||||
var connectionString = Environment.GetEnvironmentVariable("MONGODB_URI");
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
throw new InvalidOperationException("MongoDB connection string not found in environment variables");
|
||||
}
|
||||
return new MongoClient(connectionString);
|
||||
});
|
||||
|
||||
// Add MongoDB database
|
||||
builder.Services.AddSingleton(sp =>
|
||||
{
|
||||
var client = sp.GetRequiredService<IMongoClient>();
|
||||
var url = new MongoUrl(Environment.GetEnvironmentVariable("MONGODB_URI"));
|
||||
return client.GetDatabase(url.DatabaseName);
|
||||
});
|
||||
|
||||
// Add our services
|
||||
builder.Services.AddSingleton<IDiscordNotificationService, DiscordNotificationService>();
|
||||
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
||||
builder.Services.AddSingleton<IQuestionManagementService, QuestionManagementService>();
|
||||
builder.Services.AddSingleton<IAdminTokenService, AdminTokenService>();
|
||||
builder.Services.AddSingleton<IUserTokenService, UserTokenService>();
|
||||
|
||||
// Configuration
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true);
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
// Use CORS before routing
|
||||
app.UseCors("AllowClientApp");
|
||||
|
||||
var startupTime = DateTime.UtcNow - startTime;
|
||||
var process = Process.GetCurrentProcess();
|
||||
var memoryUsageMb = process.WorkingSet64 / (1024 * 1024);
|
||||
|
||||
var discordService = app.Services.GetRequiredService<IDiscordNotificationService>();
|
||||
_ = discordService.SendStartupNotification(startupTime, memoryUsageMb);
|
||||
|
||||
app.UseGlobalExceptionHandler();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
|
||||
23
Server/Server/Properties/launchSettings.json
Normal file
23
Server/Server/Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://*:9500",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7232;http://localhost:9500",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Server/Server/Server.csproj
Normal file
23
Server/Server/Server.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<RootNamespace>WritingServer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
224
Server/Server/Server.http
Normal file
224
Server/Server/Server.http
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
@baseUrl = http://localhost:9500/api
|
||||
@questionId = {{createQuestion.response.body.questionModels.id}}
|
||||
|
||||
### Exception testing
|
||||
# @name testException
|
||||
GET {{baseUrl}}/admin/test-exception
|
||||
|
||||
|
||||
### Admin Login
|
||||
# @name loginAdmin
|
||||
POST {{baseUrl}}/admin/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"loginId": "{{loginAdmin}}"
|
||||
}
|
||||
> {%
|
||||
const accessToken = response.body.accessToken;
|
||||
client.global.set("adminToken", accessToken);
|
||||
%}
|
||||
|
||||
### Admin - User Management ###
|
||||
|
||||
### List Users
|
||||
GET {{baseUrl}}/admin/users
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
### Add User
|
||||
# @name addUser
|
||||
POST {{baseUrl}}/admin/users
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "TestUser123"
|
||||
}
|
||||
|
||||
### Edit User
|
||||
PUT {{baseUrl}}/admin/users
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "TestUser123",
|
||||
"newUserName": "UpdatedUser123"
|
||||
}
|
||||
|
||||
### Reset User
|
||||
PUT {{baseUrl}}/admin/users/reset
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "UpdatedUser123"
|
||||
}
|
||||
|
||||
### Delete User
|
||||
DELETE {{baseUrl}}/admin/users
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userName": "UpdatedUser123"
|
||||
}
|
||||
|
||||
### Admin - Question Set Management ###
|
||||
|
||||
### List Question Sets
|
||||
GET {{baseUrl}}/admin/questionsets
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
### Create Question Set
|
||||
# @name createQuestionSet
|
||||
POST {{baseUrl}}/admin/questionsets
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetName": "Test Question Set"
|
||||
}
|
||||
> {%
|
||||
const questionSetId = response.body.questionSet.questionSetId;
|
||||
client.global.set("questionSetId", questionSetId);
|
||||
%}
|
||||
|
||||
### Update Question Set Name
|
||||
PUT {{baseUrl}}/admin/questionsets/name
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetId": "{{questionSetId}}",
|
||||
"newQuestionSetName": "Updated Question Set"
|
||||
}
|
||||
|
||||
### Update Question Set Order
|
||||
PUT {{baseUrl}}/admin/questionsets/order
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetId": "{{questionSetId}}",
|
||||
"questionSetOrder": 2
|
||||
}
|
||||
|
||||
### Lock/Unlock Question Set
|
||||
PUT {{baseUrl}}/admin/questionsets/lock
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetId": "{{questionSetId}}",
|
||||
"locked": true
|
||||
}
|
||||
|
||||
### Delete Question Set
|
||||
DELETE {{baseUrl}}/admin/questionsets
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetId": "{{questionSetId}}"
|
||||
}
|
||||
|
||||
### Admin - Question Management ###
|
||||
|
||||
### List Questions in a Set
|
||||
GET {{baseUrl}}/admin/questions?questionSetId={{questionSetId}}
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
### Create Question
|
||||
# @name createQuestion
|
||||
POST {{baseUrl}}/admin/questions
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionSetId": "{{questionSetId}}",
|
||||
"questionText": "What is your favorite color?",
|
||||
"expectedResultText": "A color name and explanation",
|
||||
"questionOrder": 1,
|
||||
"minWordLength": 10,
|
||||
"maxWordLength": 100
|
||||
}
|
||||
|
||||
### Update Question
|
||||
PUT {{baseUrl}}/admin/questions
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionId": "{{questionId}}",
|
||||
"questionText": "What is your favorite programming language?",
|
||||
"expectedResultText": "A programming language and why",
|
||||
"questionOrder": 1,
|
||||
"minWordLength": 20,
|
||||
"maxWordLength": 200
|
||||
}
|
||||
|
||||
### Delete Question
|
||||
DELETE {{baseUrl}}/admin/questions
|
||||
Authorization: Bearer {{adminToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionId": "{{questionId}}"
|
||||
}
|
||||
|
||||
### Admin - Responses ###
|
||||
|
||||
### Get Responses for a Question
|
||||
GET {{baseUrl}}/admin/responses?questionId={{questionId}}
|
||||
Authorization: Bearer {{adminToken}}
|
||||
|
||||
### User APIs ###
|
||||
|
||||
### User Login
|
||||
# @name loginUser
|
||||
POST {{baseUrl}}/user/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"pin": "2D21"
|
||||
}
|
||||
> {%
|
||||
const userToken = response.body.accessToken;
|
||||
client.global.set("userToken", userToken);
|
||||
%}
|
||||
|
||||
### Get Username
|
||||
GET {{baseUrl}}/user/username
|
||||
Authorization: Bearer {{userToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
### Get Reset State
|
||||
GET {{baseUrl}}/user/state
|
||||
Authorization: Bearer {{userToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
### User - Questions ###
|
||||
|
||||
### Get Question Sets
|
||||
GET {{baseUrl}}/user/questionsets
|
||||
Authorization: Bearer {{userToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
### Get Questions from Set
|
||||
# @name getQuestions
|
||||
GET {{baseUrl}}/user/questions?questionSetId={{questionSetId}}
|
||||
Authorization: Bearer {{userToken}}
|
||||
|
||||
### Submit Response
|
||||
POST {{baseUrl}}/user/responses
|
||||
Authorization: Bearer {{userToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"questionId": "{{questionId}}",
|
||||
"responseText": "This is my response to the question. Probably, what do you say?"
|
||||
}
|
||||
|
||||
### Get User Responses
|
||||
GET {{baseUrl}}/user/responses?questionId={{questionId}}
|
||||
Authorization: Bearer {{userToken}}
|
||||
24
Server/Server/Server.sln
Normal file
24
Server/Server/Server.sln
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server.csproj", "{F3EE6B2D-0B19-306C-2C68-AB272C4E49C6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{F3EE6B2D-0B19-306C-2C68-AB272C4E49C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F3EE6B2D-0B19-306C-2C68-AB272C4E49C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F3EE6B2D-0B19-306C-2C68-AB272C4E49C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F3EE6B2D-0B19-306C-2C68-AB272C4E49C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A18AA024-5978-4B91-8279-6928359C3DFD}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
22
Server/Server/Services/AdminTokenService.cs
Normal file
22
Server/Server/Services/AdminTokenService.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace WritingServer.Services;
|
||||
|
||||
public interface IAdminTokenService
|
||||
{
|
||||
void StoreToken(string token);
|
||||
bool ValidateToken(string token);
|
||||
}
|
||||
|
||||
public class AdminTokenService : IAdminTokenService
|
||||
{
|
||||
private readonly Dictionary<string, bool> _adminTokens = new();
|
||||
|
||||
public void StoreToken(string token)
|
||||
{
|
||||
_adminTokens[token] = true;
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token)
|
||||
{
|
||||
return _adminTokens.ContainsKey(token) && _adminTokens[token];
|
||||
}
|
||||
}
|
||||
62
Server/Server/Services/DiscordNotificationService.cs
Normal file
62
Server/Server/Services/DiscordNotificationService.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WritingServer.Services;
|
||||
|
||||
public interface IDiscordNotificationService
|
||||
{
|
||||
Task SendStartupNotification(TimeSpan startupTime, long memoryUsageMb);
|
||||
Task SendResponseNotification(string username, string questionSetName, string questionText, string responseText);
|
||||
Task SendExceptionNotification(string path);
|
||||
Task SendDiscordMessage(string content);
|
||||
}
|
||||
|
||||
public class DiscordNotificationService : IDiscordNotificationService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _webhookUrl;
|
||||
|
||||
public DiscordNotificationService()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
_webhookUrl = Environment.GetEnvironmentVariable("DISCORD_WEBHOOK") ?? "";
|
||||
}
|
||||
|
||||
public async Task SendStartupNotification(TimeSpan startupTime, long memoryUsageMb)
|
||||
{
|
||||
await SendDiscordMessage($"Backend warmed up!\nTook {startupTime.TotalSeconds:F2} seconds.\nMemory usage: {memoryUsageMb} MB");
|
||||
}
|
||||
|
||||
public async Task SendResponseNotification(string username, string questionSetName, string questionText, string responseText)
|
||||
{
|
||||
var message = $"New Response:\n**User:** {username}\n**Question Set:** {questionSetName}\n**Question:** {questionText}\n**Response:** {responseText}";
|
||||
await SendDiscordMessage(message);
|
||||
}
|
||||
|
||||
public async Task SendExceptionNotification(string path)
|
||||
{
|
||||
var message = $"An unhandled exception occurred at path: {path}\nMore details can be in found in `stdout`";
|
||||
await SendDiscordMessage(message);
|
||||
}
|
||||
|
||||
public async Task SendDiscordMessage(string content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_webhookUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new { content };
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var stringContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
try
|
||||
{
|
||||
await _httpClient.PostAsync(_webhookUrl, stringContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to send Discord notification: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
246
Server/Server/Services/QuestionManagementService.cs
Normal file
246
Server/Server/Services/QuestionManagementService.cs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
using MongoDB.Driver;
|
||||
using WritingServer.Models;
|
||||
using WritingServer.Utils;
|
||||
|
||||
namespace WritingServer.Services;
|
||||
|
||||
public interface IQuestionManagementService
|
||||
{
|
||||
// Question Set operations
|
||||
Task<QuestionSet> CreateQuestionSetAsync(string name);
|
||||
Task<bool> UpdateQuestionSetOrderAsync(string questionSetId, int order);
|
||||
Task<bool> UpdateQuestionSetLockStatusAsync(string questionSetId, bool locked);
|
||||
Task<bool> UpdateQuestionSetNameAsync(string questionSetId, string newName);
|
||||
Task<bool> DeleteQuestionSetAsync(string questionSetId);
|
||||
Task<QuestionSetList> GetAllQuestionSetsAsync();
|
||||
Task<QuestionSet> GetQuestionSetByIdAsync(string questionSetId);
|
||||
|
||||
// Question operations
|
||||
Task<QuestionModels> CreateQuestionAsync(string questionSetId, string questionText, string expectedResultText,
|
||||
int questionOrder, int minWordLength, int maxWordLength);
|
||||
Task<QuestionModels> UpdateQuestionAsync(string questionId, string questionText, string expectedResultText,
|
||||
int questionOrder, int minWordLength, int maxWordLength);
|
||||
Task<bool> DeleteQuestionAsync(string questionId);
|
||||
Task<QuestionModelList> GetQuestionsInSetAsync(string questionSetId);
|
||||
Task<QuestionModels> GetQuestionByIdAsync(string questionId);
|
||||
|
||||
// Response operations
|
||||
Task<bool> SubmitResponseAsync(string questionId, string userName, string responseText);
|
||||
Task<ResponseList> GetResponsesForQuestionAsync(string questionId);
|
||||
Task<ResponseList> GetResponsesForUserAsync(string userName, string questionId);
|
||||
}
|
||||
|
||||
public class QuestionManagementService : IQuestionManagementService
|
||||
{
|
||||
private readonly IMongoCollection<QuestionSet> _questionSetsCollection;
|
||||
private readonly IMongoCollection<QuestionModels> _questionsCollection;
|
||||
private readonly IMongoCollection<ResponseModels> _responsesCollection;
|
||||
private readonly IDiscordNotificationService _discordNotificationService;
|
||||
|
||||
public QuestionManagementService(IMongoDatabase database, IDiscordNotificationService discordNotificationService)
|
||||
{
|
||||
_questionSetsCollection = database.GetCollection<QuestionSet>("QuestionSets");
|
||||
_questionsCollection = database.GetCollection<QuestionModels>("Questions");
|
||||
_responsesCollection = database.GetCollection<ResponseModels>("Responses");
|
||||
_discordNotificationService = discordNotificationService;
|
||||
}
|
||||
|
||||
#region Question Set operations
|
||||
public async Task<QuestionSet> CreateQuestionSetAsync(string name)
|
||||
{
|
||||
var questionSet = new QuestionSet
|
||||
{
|
||||
QuestionSetId = TokenGenerator.GenerateToken(16),
|
||||
QuestionSetName = name,
|
||||
QuestionSetOrder = await GetNextQuestionSetOrderAsync(),
|
||||
Locked = true
|
||||
};
|
||||
|
||||
await _questionSetsCollection.InsertOneAsync(questionSet);
|
||||
return questionSet;
|
||||
}
|
||||
|
||||
private async Task<int> GetNextQuestionSetOrderAsync()
|
||||
{
|
||||
var maxOrder = await _questionSetsCollection
|
||||
.Find(_ => true)
|
||||
.SortByDescending(qs => qs.QuestionSetOrder)
|
||||
.Limit(1)
|
||||
.Project(qs => qs.QuestionSetOrder)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return maxOrder + 1;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateQuestionSetOrderAsync(string questionSetId, int order)
|
||||
{
|
||||
var result = await _questionSetsCollection.UpdateOneAsync(
|
||||
qs => qs.QuestionSetId == questionSetId,
|
||||
Builders<QuestionSet>.Update.Set(qs => qs.QuestionSetOrder, order));
|
||||
|
||||
return result.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateQuestionSetLockStatusAsync(string questionSetId, bool locked)
|
||||
{
|
||||
var result = await _questionSetsCollection.UpdateOneAsync(
|
||||
qs => qs.QuestionSetId == questionSetId,
|
||||
Builders<QuestionSet>.Update.Set(qs => qs.Locked, locked));
|
||||
|
||||
return result.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateQuestionSetNameAsync(string questionSetId, string newName)
|
||||
{
|
||||
var result = await _questionSetsCollection.UpdateOneAsync(
|
||||
qs => qs.QuestionSetId == questionSetId,
|
||||
Builders<QuestionSet>.Update.Set(qs => qs.QuestionSetName, newName));
|
||||
|
||||
return result.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteQuestionSetAsync(string questionSetId)
|
||||
{
|
||||
// First, find all questions in this set
|
||||
var questions = await _questionsCollection
|
||||
.Find(q => q.QuestionId.StartsWith(questionSetId + "_"))
|
||||
.ToListAsync();
|
||||
|
||||
// Delete all responses for questions in this set
|
||||
foreach (var question in questions)
|
||||
{
|
||||
await _responsesCollection.DeleteManyAsync(r => r.QuestionId == question.QuestionId);
|
||||
}
|
||||
|
||||
// Delete all questions in this set
|
||||
await _questionsCollection.DeleteManyAsync(q => q.QuestionId.StartsWith(questionSetId + "_"));
|
||||
|
||||
// Delete the question set
|
||||
var result = await _questionSetsCollection.DeleteOneAsync(qs => qs.QuestionSetId == questionSetId);
|
||||
return result.DeletedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<QuestionSetList> GetAllQuestionSetsAsync()
|
||||
{
|
||||
var questionSets = await _questionSetsCollection
|
||||
.Find(_ => true)
|
||||
.SortBy(qs => qs.QuestionSetOrder)
|
||||
.ToListAsync();
|
||||
|
||||
return new QuestionSetList { QuestionSets = questionSets };
|
||||
}
|
||||
|
||||
public async Task<QuestionSet> GetQuestionSetByIdAsync(string questionSetId)
|
||||
{
|
||||
return await _questionSetsCollection
|
||||
.Find(qs => qs.QuestionSetId == questionSetId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Question operations
|
||||
public async Task<QuestionModels> CreateQuestionAsync(string questionSetId, string questionText, string expectedResultText,
|
||||
int questionOrder, int minWordLength, int maxWordLength)
|
||||
{
|
||||
var question = new QuestionModels
|
||||
{
|
||||
QuestionId = $"{questionSetId}_{TokenGenerator.GenerateToken(8)}",
|
||||
QuestionText = questionText,
|
||||
ExpectedResultText = expectedResultText,
|
||||
QuestionOrder = questionOrder,
|
||||
MinWordLength = minWordLength,
|
||||
MaxWordLength = maxWordLength
|
||||
};
|
||||
|
||||
await _questionsCollection.InsertOneAsync(question);
|
||||
return question;
|
||||
}
|
||||
|
||||
public async Task<QuestionModels> UpdateQuestionAsync(string questionId, string questionText, string expectedResultText,
|
||||
int questionOrder, int minWordLength, int maxWordLength)
|
||||
{
|
||||
var update = Builders<QuestionModels>.Update
|
||||
.Set(q => q.QuestionText, questionText)
|
||||
.Set(q => q.ExpectedResultText, expectedResultText)
|
||||
.Set(q => q.QuestionOrder, questionOrder)
|
||||
.Set(q => q.MinWordLength, minWordLength)
|
||||
.Set(q => q.MaxWordLength, maxWordLength);
|
||||
|
||||
await _questionsCollection.UpdateOneAsync(q => q.QuestionId == questionId, update);
|
||||
return await GetQuestionByIdAsync(questionId);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteQuestionAsync(string questionId)
|
||||
{
|
||||
await _responsesCollection.DeleteManyAsync(r => r.QuestionId == questionId);
|
||||
|
||||
var result = await _questionsCollection.DeleteOneAsync(q => q.QuestionId == questionId);
|
||||
return result.DeletedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<QuestionModelList> GetQuestionsInSetAsync(string questionSetId)
|
||||
{
|
||||
var questions = await _questionsCollection
|
||||
.Find(q => q.QuestionId.StartsWith(questionSetId + "_"))
|
||||
.SortBy(q => q.QuestionOrder)
|
||||
.ToListAsync();
|
||||
|
||||
return new QuestionModelList { Questions = questions };
|
||||
}
|
||||
|
||||
public async Task<QuestionModels> GetQuestionByIdAsync(string questionId)
|
||||
{
|
||||
return await _questionsCollection
|
||||
.Find(q => q.QuestionId == questionId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Response operations
|
||||
public async Task<bool> SubmitResponseAsync(string questionId, string userName, string responseText)
|
||||
{
|
||||
var response = new ResponseModels
|
||||
{
|
||||
ResponseId = TokenGenerator.GenerateToken(16),
|
||||
UserName = userName,
|
||||
QuestionId = questionId,
|
||||
ResponseText = responseText,
|
||||
ResponseTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _responsesCollection.InsertOneAsync(response);
|
||||
|
||||
// Discord webhook
|
||||
var question = await GetQuestionByIdAsync(questionId);
|
||||
string questionSetId = questionId.Split('_')[0];
|
||||
var questionSet = await GetQuestionSetByIdAsync(questionSetId);
|
||||
await _discordNotificationService.SendResponseNotification(
|
||||
userName,
|
||||
questionSet?.QuestionSetName ?? "Unknown Set",
|
||||
question?.QuestionText ?? "Unknown Question",
|
||||
responseText);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<ResponseList> GetResponsesForQuestionAsync(string questionId)
|
||||
{
|
||||
var responses = await _responsesCollection
|
||||
.Find(r => r.QuestionId == questionId)
|
||||
.SortByDescending(r => r.ResponseTime)
|
||||
.ToListAsync();
|
||||
|
||||
return new ResponseList { Response = responses };
|
||||
}
|
||||
|
||||
public async Task<ResponseList> GetResponsesForUserAsync(string userName, string questionId)
|
||||
{
|
||||
var responses = await _responsesCollection
|
||||
.Find(r => r.QuestionId == questionId && r.UserName == userName)
|
||||
.SortByDescending(r => r.ResponseTime)
|
||||
.ToListAsync();
|
||||
|
||||
return new ResponseList { Response = responses };
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
138
Server/Server/Services/UserManagementService.cs
Normal file
138
Server/Server/Services/UserManagementService.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
using MongoDB.Driver;
|
||||
using WritingServer.Models;
|
||||
using WritingServer.Utils;
|
||||
|
||||
namespace WritingServer.Services;
|
||||
|
||||
public interface IUserManagementService
|
||||
{
|
||||
Task<UserModel> AddUserAsync(string userName);
|
||||
Task<bool> DeleteUserAsync(string userName);
|
||||
Task<bool> UpdateUserNameAsync(string currentUserName, string newUserName);
|
||||
Task<bool> ResetUserAsync(string userName);
|
||||
Task<UserModelList> GetAllUsersAsync();
|
||||
Task<UserModel?> GetUserByPinAsync(string pin);
|
||||
Task<UserModel?> GetUserByTokenAsync(string accessToken);
|
||||
Task<UserModel?> GetUserByNameAsync(string userName);
|
||||
string GenerateAccessToken(string username);
|
||||
void ResetLoginState(string userName);
|
||||
}
|
||||
|
||||
public class UserManagementService : IUserManagementService
|
||||
{
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly IMongoCollection<UserModel> _usersCollection;
|
||||
private readonly IMongoCollection<ResponseModels> _responsesCollection;
|
||||
private readonly IUserTokenService _tokenService;
|
||||
|
||||
public UserManagementService(IMongoDatabase database, IUserTokenService tokenService)
|
||||
{
|
||||
_database = database;
|
||||
_usersCollection = database.GetCollection<UserModel>("Users");
|
||||
_responsesCollection = database.GetCollection<ResponseModels>("Responses");
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
public async Task<UserModel> AddUserAsync(string userName)
|
||||
{
|
||||
var existingUser = await _usersCollection.Find(u => u.Username == userName).FirstOrDefaultAsync();
|
||||
if (existingUser != null)
|
||||
{
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
var pin = TokenGenerator.IdGenerator(4);
|
||||
|
||||
var user = new UserModel
|
||||
{
|
||||
Username = userName,
|
||||
Pin = pin,
|
||||
ResetState = false
|
||||
};
|
||||
|
||||
await _usersCollection.InsertOneAsync(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserAsync(string userName)
|
||||
{
|
||||
var deleteResponseResult = await _responsesCollection.DeleteManyAsync(r => r.UserName == userName);
|
||||
|
||||
var deleteUserResult = await _usersCollection.DeleteOneAsync(u => u.Username == userName);
|
||||
_tokenService.RemoveTokensForUser(userName);
|
||||
|
||||
return deleteUserResult.DeletedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateUserNameAsync(string currentUserName, string newUserName)
|
||||
{
|
||||
var existingUser = await _usersCollection.Find(u => u.Username == newUserName).FirstOrDefaultAsync();
|
||||
if (existingUser != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update username
|
||||
var updateResult = await _usersCollection.UpdateOneAsync(
|
||||
u => u.Username == currentUserName,
|
||||
Builders<UserModel>.Update.Set(u => u.Username, newUserName));
|
||||
|
||||
// Update references in responses
|
||||
await _responsesCollection.UpdateManyAsync(
|
||||
r => r.UserName == currentUserName,
|
||||
Builders<ResponseModels>.Update.Set(r => r.UserName, newUserName));
|
||||
|
||||
_tokenService.UpdateUserInTokens(currentUserName, newUserName);
|
||||
|
||||
return updateResult.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> ResetUserAsync(string userName)
|
||||
{
|
||||
await _responsesCollection.DeleteManyAsync(r => r.UserName == userName);
|
||||
|
||||
// Set reset flag
|
||||
var updateResult = await _usersCollection.UpdateOneAsync(
|
||||
u => u.Username == userName,
|
||||
Builders<UserModel>.Update.Set(u => u.ResetState, true));
|
||||
|
||||
return updateResult.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<UserModelList> GetAllUsersAsync()
|
||||
{
|
||||
var users = await _usersCollection.Find(_ => true).ToListAsync();
|
||||
return new UserModelList { Users = users };
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetUserByPinAsync(string pin)
|
||||
{
|
||||
return await _usersCollection.Find(u => u.Pin == pin).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetUserByNameAsync(string userName)
|
||||
{
|
||||
return await _usersCollection.Find(u => u.Username == userName).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<UserModel?> GetUserByTokenAsync(string accessToken)
|
||||
{
|
||||
var username = _tokenService.GetUsernameFromToken(accessToken);
|
||||
return username != null ? await GetUserByNameAsync(username) : null;
|
||||
}
|
||||
|
||||
// Helper methods for token management
|
||||
public string GenerateAccessToken(string userName)
|
||||
{
|
||||
var token = TokenGenerator.GenerateToken(32);
|
||||
_tokenService.StoreToken(token, userName);
|
||||
return token;
|
||||
}
|
||||
|
||||
public void ResetLoginState(string userName)
|
||||
{
|
||||
_usersCollection.UpdateOne(
|
||||
u => u.Username == userName,
|
||||
Builders<UserModel>.Update.Set(u => u.ResetState, false));
|
||||
}
|
||||
}
|
||||
60
Server/Server/Services/UserTokenService.cs
Normal file
60
Server/Server/Services/UserTokenService.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
namespace WritingServer.Services;
|
||||
|
||||
public interface IUserTokenService
|
||||
{
|
||||
void StoreToken(string token, string username);
|
||||
string? GetUsernameFromToken(string token);
|
||||
bool ValidateToken(string token);
|
||||
void ClearToken(string token);
|
||||
void RemoveTokensForUser(string username);
|
||||
void UpdateUserInTokens(string oldUsername, string newUsername);
|
||||
}
|
||||
|
||||
public class UserTokenService : IUserTokenService
|
||||
{
|
||||
private readonly Dictionary<string, string> _userTokens = new(); // token -> username
|
||||
|
||||
public void StoreToken(string token, string username)
|
||||
{
|
||||
_userTokens[token] = username;
|
||||
}
|
||||
|
||||
public string? GetUsernameFromToken(string token)
|
||||
{
|
||||
return _userTokens.TryGetValue(token, out var username) ? username : null;
|
||||
}
|
||||
|
||||
public bool ValidateToken(string token)
|
||||
{
|
||||
return _userTokens.ContainsKey(token);
|
||||
}
|
||||
|
||||
public void ClearToken(string token)
|
||||
{
|
||||
_userTokens.Remove(token);
|
||||
}
|
||||
|
||||
public void RemoveTokensForUser(string username)
|
||||
{
|
||||
var tokensToRemove = _userTokens
|
||||
.Where(kvp => kvp.Value == username)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var token in tokensToRemove)
|
||||
{
|
||||
_userTokens.Remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateUserInTokens(string oldUsername, string newUsername)
|
||||
{
|
||||
foreach (var key in _userTokens.Keys.ToList())
|
||||
{
|
||||
if (_userTokens[key] == oldUsername)
|
||||
{
|
||||
_userTokens[key] = newUsername;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Server/Server/Utils/GlobalExceptionHandlerMiddleware.cs
Normal file
59
Server/Server/Utils/GlobalExceptionHandlerMiddleware.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using WritingServer.Services;
|
||||
|
||||
namespace WritingServer.Utils;
|
||||
|
||||
public class GlobalExceptionHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IDiscordNotificationService _discordService;
|
||||
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public GlobalExceptionHandlerMiddleware(
|
||||
RequestDelegate next,
|
||||
IDiscordNotificationService discordService,
|
||||
ILogger<GlobalExceptionHandlerMiddleware> logger,
|
||||
IWebHostEnvironment environment)
|
||||
{
|
||||
_next = next;
|
||||
_discordService = discordService;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An unhandled exception occurred while processing request to {Path}", context.Request.Path);
|
||||
await _discordService.SendExceptionNotification(context.Request.Path);
|
||||
await HandleExceptionAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleExceptionAsync(HttpContext context)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
|
||||
var response = new
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "An unexpected error occurred. Please try again later."
|
||||
};
|
||||
|
||||
await context.Response.WriteAsJsonAsync(response);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GlobalExceptionHandlerMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<GlobalExceptionHandlerMiddleware>();
|
||||
}
|
||||
}
|
||||
32
Server/Server/Utils/TokenGenerator.cs
Normal file
32
Server/Server/Utils/TokenGenerator.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace WritingServer.Utils;
|
||||
|
||||
public static class TokenGenerator
|
||||
{
|
||||
public static string IdGenerator(int length)
|
||||
{
|
||||
Random random = new Random();
|
||||
string numbers = "0123456789ABCDEF";
|
||||
string chars = string.Empty;
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
chars += numbers[random.Next(numbers.Length)];
|
||||
}
|
||||
return chars;
|
||||
}
|
||||
|
||||
public static string GenerateToken(int length)
|
||||
{
|
||||
byte[] randomBytes = new byte[length / 2];
|
||||
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(randomBytes);
|
||||
}
|
||||
|
||||
string hexString = Convert.ToHexString(randomBytes);
|
||||
|
||||
return hexString.Length > length ? hexString.Substring(0, length) : hexString;
|
||||
}
|
||||
}
|
||||
594
Server/Server/apiDoc.md
Normal file
594
Server/Server/apiDoc.md
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
# WritingServer API Documentation
|
||||
|
||||
This document outlines all available API endpoints in the WritingServer application.
|
||||
|
||||
## Table of Contents
|
||||
- [Authentication](#authentication)
|
||||
- [Admin API](#admin-api)
|
||||
- [User Management](#admin-user-management)
|
||||
- [Question Set Management](#admin-question-set-management)
|
||||
- [Question Management](#admin-question-management)
|
||||
- [Response Management](#admin-response-management)
|
||||
- [User API](#user-api)
|
||||
- [Profile Management](#user-profile-management)
|
||||
- [Questions](#user-questions)
|
||||
- [Responses](#user-responses)
|
||||
|
||||
## Authentication
|
||||
|
||||
### Admin Login
|
||||
Authenticates an admin user and returns an access token.
|
||||
|
||||
**Endpoint:** `POST /api/admin/login`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"loginId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"accessToken": "string",
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### User Login
|
||||
Authenticates a user with their PIN and returns an access token.
|
||||
|
||||
**Endpoint:** `POST /api/user/login`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"pin": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"accessToken": "string",
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
## Admin API
|
||||
|
||||
All admin endpoints require authentication using the `Authorization: Bearer {token}` header.
|
||||
|
||||
### Admin User Management
|
||||
|
||||
#### List Users
|
||||
Returns a list of all users.
|
||||
|
||||
**Endpoint:** `GET /api/admin/users`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"users": {
|
||||
"users": [
|
||||
{
|
||||
"username": "string",
|
||||
"pin": "string",
|
||||
"resetState": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Add User
|
||||
Creates a new user with the specified username.
|
||||
|
||||
**Endpoint:** `POST /api/admin/users`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Edit User
|
||||
Updates a user's username.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/users`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userName": "string",
|
||||
"newUserName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Reset User
|
||||
Resets a user's responses and sets their reset state.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/users/reset`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete User
|
||||
Deletes a user and all associated responses.
|
||||
|
||||
**Endpoint:** `DELETE /api/admin/users`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Question Set Management
|
||||
|
||||
#### List Question Sets
|
||||
Returns all question sets.
|
||||
|
||||
**Endpoint:** `GET /api/admin/questionsets`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSets": {
|
||||
"questionSets": [
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Question Set
|
||||
Creates a new question set.
|
||||
|
||||
**Endpoint:** `POST /api/admin/questionsets`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSet": {
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": false
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Question Set Name
|
||||
Updates a question set's name.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/questionsets/name`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"newQuestionSetName": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSet": {
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": false
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Question Set Order
|
||||
Updates a question set's display order.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/questionsets/order`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"questionSetOrder": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSet": {
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": false
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Lock/Unlock Question Set
|
||||
Toggles a question set's locked status.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/questionsets/lock`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"locked": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSet": {
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": true
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete Question Set
|
||||
Deletes a question set and all associated questions.
|
||||
|
||||
**Endpoint:** `DELETE /api/admin/questionsets`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Question Management
|
||||
|
||||
#### List Questions in a Set
|
||||
Returns all questions in a specified question set.
|
||||
|
||||
**Endpoint:** `GET /api/admin/questions?questionSetId={questionSetId}`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questions": {
|
||||
"questions": [
|
||||
{
|
||||
"questionId": "string",
|
||||
"questionSetId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Question
|
||||
Creates a new question in a question set.
|
||||
|
||||
**Endpoint:** `POST /api/admin/questions`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionModels": {
|
||||
"questionId": "string",
|
||||
"questionSetId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Update Question
|
||||
Updates an existing question.
|
||||
|
||||
**Endpoint:** `PUT /api/admin/questions`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionModels": {
|
||||
"questionId": "string",
|
||||
"questionSetId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete Question
|
||||
Deletes a question and associated responses.
|
||||
|
||||
**Endpoint:** `DELETE /api/admin/questions`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Response Management
|
||||
|
||||
#### Get Responses for a Question
|
||||
Returns all user responses for a specific question.
|
||||
|
||||
**Endpoint:** `GET /api/admin/responses?questionId={questionId}`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responses": {
|
||||
"responses": [
|
||||
{
|
||||
"responseId": "string",
|
||||
"questionId": "string",
|
||||
"userName": "string",
|
||||
"responseText": "string",
|
||||
"timestamp": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
## User API
|
||||
|
||||
All user endpoints require authentication using the `Authorization: Bearer {token}` header.
|
||||
|
||||
### User Profile Management
|
||||
|
||||
#### Get Username
|
||||
Returns the current user's username.
|
||||
|
||||
**Endpoint:** `GET /api/user/username`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"username": "string",
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Reset State
|
||||
Returns whether the user's account has been reset.
|
||||
|
||||
**Endpoint:** `GET /api/user/state`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"resetState": false,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### User Questions
|
||||
|
||||
#### Get Question Sets
|
||||
Returns all unlocked question sets.
|
||||
|
||||
**Endpoint:** `GET /api/user/questionsets`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questionSets": {
|
||||
"questionSets": [
|
||||
{
|
||||
"questionSetId": "string",
|
||||
"questionSetName": "string",
|
||||
"questionSetOrder": 0,
|
||||
"locked": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Questions from Set
|
||||
Returns all questions in a specified question set.
|
||||
|
||||
**Endpoint:** `GET /api/user/questions?questionSetId={questionSetId}`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"questions": {
|
||||
"questions": [
|
||||
{
|
||||
"questionId": "string",
|
||||
"questionSetId": "string",
|
||||
"questionText": "string",
|
||||
"expectedResultText": "string",
|
||||
"questionOrder": 0,
|
||||
"minWordLength": 0,
|
||||
"maxWordLength": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
### User Responses
|
||||
|
||||
#### Submit Response
|
||||
Submits a user's response to a question.
|
||||
|
||||
**Endpoint:** `POST /api/user/responses`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"questionId": "string",
|
||||
"responseText": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Get User Responses
|
||||
Returns all of the current user's responses to a specific question.
|
||||
|
||||
**Endpoint:** `GET /api/user/responses?questionId={questionId}`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"responses": {
|
||||
"responses": [
|
||||
{
|
||||
"responseId": "string",
|
||||
"questionId": "string",
|
||||
"userName": "string",
|
||||
"responseText": "string",
|
||||
"timestamp": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
12
Server/Server/appsettings.Development.json
Normal file
12
Server/Server/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"DefaultAdminUserName": "admin",
|
||||
"DefaultAdminPassword": "password",
|
||||
"IdLength": 8,
|
||||
"TokenLength": 1024
|
||||
}
|
||||
13
Server/Server/appsettings.json
Normal file
13
Server/Server/appsettings.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"DefaultAdminUserName": "admin",
|
||||
"DefaultAdminPassword": "password",
|
||||
"IdLength": 8,
|
||||
"TokenLength": 1024
|
||||
}
|
||||
8
Server/Server/http-client.private.env.json
Normal file
8
Server/Server/http-client.private.env.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dev": {
|
||||
"loginAdmin": "admin123",
|
||||
"loginUser": "value",
|
||||
"createQuestionSet": "value",
|
||||
"createQuestion": "value"
|
||||
}
|
||||
}
|
||||
12
Server/docker-compose-build.yaml
Normal file
12
Server/docker-compose-build.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
services:
|
||||
quizconnectserver:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- APP_UID=1000
|
||||
ports:
|
||||
- "9500:9500"
|
||||
volumes:
|
||||
- ./Server/.env:/app/.env
|
||||
restart: unless-stopped
|
||||
11
Server/docker-compose.yaml
Normal file
11
Server/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
services:
|
||||
quizconnectserver:
|
||||
image: ghcr.io/marcus7i/quizconnect:latest
|
||||
container_name: quizconnectserver
|
||||
environment:
|
||||
- APP_UID=1000
|
||||
ports:
|
||||
- "9500:9500"
|
||||
volumes:
|
||||
- ./Server/.env:/app/.env
|
||||
restart: unless-stopped
|
||||
Loading…
Add table
Add a link
Reference in a new issue