Skip to content

Commit 0644985

Browse files
Add File management CRUD API and client service
1 parent 848355f commit 0644985

File tree

8 files changed

+422
-0
lines changed

8 files changed

+422
-0
lines changed

Client/Services/FileService.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to ICTAce under the MIT license.
2+
3+
namespace ICTAce.FileHub.Services;
4+
5+
public record GetFileDto
6+
{
7+
public int Id { get; set; }
8+
public int ModuleId { get; set; }
9+
public required string Name { get; set; }
10+
public required string FileName { get; set; }
11+
public required string ImageName { get; set; }
12+
public string? Description { get; set; }
13+
public required string FileSize { get; set; }
14+
public int Downloads { get; set; }
15+
16+
public required string CreatedBy { get; set; }
17+
public required DateTime CreatedOn { get; set; }
18+
public required string ModifiedBy { get; set; }
19+
public required DateTime ModifiedOn { get; set; }
20+
}
21+
22+
public record ListFileDto
23+
{
24+
public int Id { get; set; }
25+
public required string Name { get; set; }
26+
public required string FileName { get; set; }
27+
public required string ImageName { get; set; }
28+
public string? Description { get; set; }
29+
public required string FileSize { get; set; }
30+
public int Downloads { get; set; }
31+
}
32+
33+
public record CreateAndUpdateFileDto
34+
{
35+
[Required(ErrorMessage = "Name is required")]
36+
[StringLength(100, MinimumLength = 1, ErrorMessage = "Name must be between 1 and 100 characters")]
37+
public string Name { get; set; } = string.Empty;
38+
39+
[Required(ErrorMessage = "FileName is required")]
40+
[StringLength(255, MinimumLength = 1, ErrorMessage = "FileName must be between 1 and 255 characters")]
41+
public string FileName { get; set; } = string.Empty;
42+
43+
[Required(ErrorMessage = "ImageName is required")]
44+
[StringLength(255, MinimumLength = 1, ErrorMessage = "ImageName must be between 1 and 255 characters")]
45+
public string ImageName { get; set; } = string.Empty;
46+
47+
[StringLength(1000, ErrorMessage = "Description must not exceed 1000 characters")]
48+
public string? Description { get; set; }
49+
50+
[Required(ErrorMessage = "FileSize is required")]
51+
[StringLength(12, MinimumLength = 1, ErrorMessage = "FileSize must be between 1 and 12 characters")]
52+
public string FileSize { get; set; } = string.Empty;
53+
54+
[Range(0, int.MaxValue, ErrorMessage = "Downloads must be greater than or equal to 0")]
55+
public int Downloads { get; set; }
56+
}
57+
58+
public interface IFileService
59+
{
60+
Task<GetFileDto> GetAsync(int id, int moduleId);
61+
Task<PagedResult<ListFileDto>> ListAsync(int moduleId, int pageNumber = 1, int pageSize = 10);
62+
Task<int> CreateAsync(int moduleId, CreateAndUpdateFileDto dto);
63+
Task<int> UpdateAsync(int id, int moduleId, CreateAndUpdateFileDto dto);
64+
Task DeleteAsync(int id, int moduleId);
65+
}
66+
67+
public class FileService(HttpClient http, SiteState siteState)
68+
: ModuleService<GetFileDto, ListFileDto, CreateAndUpdateFileDto>(http, siteState, "ictace/fileHub/files"),
69+
IFileService
70+
{
71+
}

Client/Startup/ClientStartup.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public void ConfigureServices(IServiceCollection services)
1818
services.AddScoped<ICategoryService, CategoryService>();
1919
}
2020

21+
if (!services.Any(s => s.ServiceType == typeof(Services.IFileService)))
22+
{
23+
services.AddScoped<Services.IFileService, Services.FileService>();
24+
}
25+
2126
services.AddRadzenComponents();
2227
}
2328
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Licensed to ICTAce under the MIT license.
2+
3+
namespace ICTAce.FileHub.Features.Files;
4+
5+
[Route("api/ictace/fileHub/files")]
6+
[ApiController]
7+
public class ICTAceFileHubFilesController(
8+
IMediator mediator,
9+
ILogManager logger,
10+
IHttpContextAccessor accessor)
11+
: ModuleControllerBase(logger, accessor)
12+
{
13+
private readonly IMediator _mediator = mediator;
14+
15+
[HttpGet("{id}")]
16+
[Authorize(Policy = PolicyNames.ViewModule)]
17+
[ProducesResponseType(typeof(GetFileDto), StatusCodes.Status200OK)]
18+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
19+
[ProducesResponseType(StatusCodes.Status404NotFound)]
20+
public async Task<ActionResult<GetFileDto>> GetAsync(
21+
int id,
22+
[FromQuery] int moduleId,
23+
CancellationToken cancellationToken = default)
24+
{
25+
if (!IsAuthorizedEntityId(EntityNames.Module, moduleId))
26+
{
27+
_logger.Log(LogLevel.Error, this, LogFunction.Security,
28+
"Unauthorized FileHub File Get Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId);
29+
return Forbid();
30+
}
31+
32+
if (id <= 0)
33+
{
34+
return BadRequest("Invalid File ID");
35+
}
36+
37+
var query = new GetFileRequest
38+
{
39+
ModuleId = moduleId,
40+
Id = id,
41+
};
42+
43+
var file = await _mediator.Send(query, cancellationToken).ConfigureAwait(false);
44+
45+
if (file is null)
46+
{
47+
_logger.Log(LogLevel.Warning, this, LogFunction.Read,
48+
"File Not Found Id={Id} in ModuleId={ModuleId}", id, moduleId);
49+
return NotFound();
50+
}
51+
52+
return Ok(file);
53+
}
54+
55+
[HttpGet("")]
56+
[Authorize(Policy = PolicyNames.ViewModule)]
57+
[ProducesResponseType(typeof(PagedResult<ListFileDto>), StatusCodes.Status200OK)]
58+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
59+
public async Task<ActionResult<PagedResult<ListFileDto>>> ListAsync(
60+
[FromQuery] int moduleId,
61+
[FromQuery] int pageNumber = 1,
62+
[FromQuery] int pageSize = 10,
63+
CancellationToken cancellationToken = default)
64+
{
65+
if (!IsAuthorizedEntityId(EntityNames.Module, moduleId))
66+
{
67+
_logger.Log(LogLevel.Error, this, LogFunction.Security,
68+
"Unauthorized File List Attempt ModuleId={ModuleId}", moduleId);
69+
return Forbid();
70+
}
71+
72+
if (pageSize > 100)
73+
{
74+
pageSize = 100;
75+
}
76+
77+
if (pageNumber < 1)
78+
{
79+
pageNumber = 1;
80+
}
81+
82+
var query = new ListFileRequest
83+
{
84+
ModuleId = moduleId,
85+
PageNumber = pageNumber,
86+
PageSize = pageSize,
87+
};
88+
89+
var result = await _mediator.Send(query, cancellationToken).ConfigureAwait(false);
90+
91+
if (result is null)
92+
{
93+
return NotFound();
94+
}
95+
96+
return Ok(result);
97+
}
98+
99+
[HttpPost("")]
100+
[Authorize(Policy = PolicyNames.EditModule)]
101+
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
102+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
103+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
104+
public async Task<ActionResult<int>> CreateAsync(
105+
[FromQuery] int moduleId,
106+
[FromBody] CreateAndUpdateFileDto dto,
107+
CancellationToken cancellationToken = default)
108+
{
109+
if (!ModelState.IsValid)
110+
{
111+
return BadRequest(ModelState);
112+
}
113+
114+
if (!IsAuthorizedEntityId(EntityNames.Module, moduleId))
115+
{
116+
_logger.Log(LogLevel.Error, this, LogFunction.Security,
117+
"Unauthorized File Create Attempt ModuleId={ModuleId}", moduleId);
118+
return Forbid();
119+
}
120+
121+
var command = new CreateFileRequest
122+
{
123+
ModuleId = moduleId,
124+
Name = dto.Name,
125+
FileName = dto.FileName,
126+
ImageName = dto.ImageName,
127+
Description = dto.Description,
128+
FileSize = dto.FileSize,
129+
Downloads = dto.Downloads,
130+
};
131+
132+
var id = await _mediator.Send(command, cancellationToken).ConfigureAwait(false);
133+
134+
return Created(
135+
Url.Action(nameof(GetAsync), new { id, moduleId = command.ModuleId }) ?? string.Empty,
136+
id);
137+
}
138+
139+
[HttpPut("{id}")]
140+
[Authorize(Policy = PolicyNames.EditModule)]
141+
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
142+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
143+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
144+
public async Task<ActionResult<int>> UpdateAsync(
145+
int id,
146+
[FromQuery] int moduleId,
147+
[FromBody] CreateAndUpdateFileDto dto,
148+
CancellationToken cancellationToken = default)
149+
{
150+
if (!ModelState.IsValid)
151+
{
152+
return BadRequest(ModelState);
153+
}
154+
155+
if (!IsAuthorizedEntityId(EntityNames.Module, moduleId))
156+
{
157+
_logger.Log(LogLevel.Error, this, LogFunction.Security,
158+
"Unauthorized File Update Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId);
159+
return Forbid();
160+
}
161+
162+
var command = new UpdateFileRequest
163+
{
164+
Id = id,
165+
ModuleId = moduleId,
166+
Name = dto.Name,
167+
FileName = dto.FileName,
168+
ImageName = dto.ImageName,
169+
Description = dto.Description,
170+
FileSize = dto.FileSize,
171+
Downloads = dto.Downloads,
172+
};
173+
174+
var result = await _mediator.Send(command, cancellationToken).ConfigureAwait(false);
175+
176+
return Ok(result);
177+
}
178+
179+
[HttpDelete("{id}")]
180+
[Authorize(Policy = PolicyNames.EditModule)]
181+
[ProducesResponseType(StatusCodes.Status204NoContent)]
182+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
183+
public async Task<IActionResult> DeleteAsync(
184+
int id,
185+
[FromQuery] int moduleId,
186+
CancellationToken cancellationToken = default)
187+
{
188+
if (!IsAuthorizedEntityId(EntityNames.Module, moduleId))
189+
{
190+
_logger.Log(LogLevel.Error, this, LogFunction.Security,
191+
"Unauthorized File Delete Attempt Id={Id} in ModuleId={ModuleId}", id, moduleId);
192+
return Forbid();
193+
}
194+
195+
if (id <= 0)
196+
{
197+
return BadRequest("Invalid File ID");
198+
}
199+
200+
var command = new DeleteFileRequest
201+
{
202+
ModuleId = moduleId,
203+
Id = id,
204+
};
205+
206+
await _mediator.Send(command, cancellationToken).ConfigureAwait(false);
207+
208+
return NoContent();
209+
}
210+
}

Server/Features/Files/Create.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to ICTAce under the MIT license.
2+
3+
namespace ICTAce.FileHub.Features.Files;
4+
5+
public record CreateFileRequest : RequestBase, IRequest<int>
6+
{
7+
public string Name { get; set; } = string.Empty;
8+
public string FileName { get; set; } = string.Empty;
9+
public string ImageName { get; set; } = string.Empty;
10+
public string? Description { get; set; }
11+
public string FileSize { get; set; } = string.Empty;
12+
public int Downloads { get; set; }
13+
}
14+
15+
public class CreateHandler(HandlerServices<ApplicationCommandContext> services)
16+
: HandlerBase<ApplicationCommandContext>(services), IRequestHandler<CreateFileRequest, int>
17+
{
18+
private static readonly CreateMapper _mapper = new();
19+
20+
public Task<int> Handle(CreateFileRequest request, CancellationToken cancellationToken)
21+
{
22+
return HandleCreateAsync(
23+
request: request,
24+
mapToEntity: _mapper.ToEntity,
25+
cancellationToken: cancellationToken
26+
);
27+
}
28+
}
29+
30+
[Mapper]
31+
internal sealed partial class CreateMapper
32+
{
33+
internal partial ICTAce.FileHub.Persistence.Entities.File ToEntity(CreateFileRequest request);
34+
}

Server/Features/Files/Delete.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to ICTAce under the MIT license.
2+
3+
namespace ICTAce.FileHub.Features.Files;
4+
5+
public record DeleteFileRequest : EntityRequestBase, IRequest<int>;
6+
7+
public class DeleteHandler(HandlerServices<ApplicationCommandContext> services)
8+
: HandlerBase<ApplicationCommandContext>(services), IRequestHandler<DeleteFileRequest, int>
9+
{
10+
public Task<int> Handle(DeleteFileRequest request, CancellationToken cancellationToken)
11+
{
12+
return HandleDeleteAsync<DeleteFileRequest, Persistence.Entities.File>(
13+
request: request,
14+
cancellationToken: cancellationToken
15+
);
16+
}
17+
}

Server/Features/Files/Get.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to ICTAce under the MIT license.
2+
3+
namespace ICTAce.FileHub.Features.Files;
4+
5+
public record GetFileRequest : EntityRequestBase, IRequest<GetFileDto>;
6+
7+
public class GetHandler(HandlerServices<ApplicationQueryContext> services)
8+
: HandlerBase<ApplicationQueryContext>(services), IRequestHandler<GetFileRequest, GetFileDto?>
9+
{
10+
private static readonly GetMapper _mapper = new();
11+
12+
public Task<GetFileDto?> Handle(GetFileRequest request, CancellationToken cancellationToken)
13+
{
14+
return HandleGetAsync<GetFileRequest, Persistence.Entities.File, GetFileDto>(
15+
request: request,
16+
mapToResponse: _mapper.ToGetResponse,
17+
cancellationToken: cancellationToken
18+
);
19+
}
20+
}
21+
22+
[Mapper]
23+
internal sealed partial class GetMapper
24+
{
25+
public partial GetFileDto ToGetResponse(ICTAce.FileHub.Persistence.Entities.File file);
26+
}

0 commit comments

Comments
 (0)