diff --git a/ODPC.Server/Apis/Odrc/OdrcClientFactory.cs b/ODPC.Server/Apis/Odrc/OdrcClientFactory.cs index a7d561a..6b68896 100644 --- a/ODPC.Server/Apis/Odrc/OdrcClientFactory.cs +++ b/ODPC.Server/Apis/Odrc/OdrcClientFactory.cs @@ -5,12 +5,12 @@ namespace ODPC.Apis.Odrc { public interface IOdrcClientFactory { - HttpClient Create(OdpUser user, string handeling); + HttpClient Create(string handeling); } - public class OdrcClientFactory(IHttpClientFactory httpClientFactory, IConfiguration config) : IOdrcClientFactory + public class OdrcClientFactory(IHttpClientFactory httpClientFactory, IConfiguration config, OdpcUser user) : IOdrcClientFactory { - public HttpClient Create(OdpUser user, string? handeling) + public HttpClient Create(string? handeling) { var client = httpClientFactory.CreateClient(); client.BaseAddress = new(config["ODRC_BASE_URL"]!); diff --git a/ODPC.Server/Apis/Odrc/PagedResponseModel.cs b/ODPC.Server/Apis/Odrc/PagedResponseModel.cs index fa7a067..d8a350a 100644 --- a/ODPC.Server/Apis/Odrc/PagedResponseModel.cs +++ b/ODPC.Server/Apis/Odrc/PagedResponseModel.cs @@ -2,7 +2,7 @@ { public class PagedResponseModel { - public List results { get; set; } + public required IReadOnlyList Results { get; set; } public int Count { get; set; } public string? Next { get; set; } public string? Previous { get; set; } diff --git a/ODPC.Server/Apis/Odrc/WaardelijstResponseModel.cs b/ODPC.Server/Apis/Odrc/WaardelijstResponseModel.cs index fecf0d8..bba2f9e 100644 --- a/ODPC.Server/Apis/Odrc/WaardelijstResponseModel.cs +++ b/ODPC.Server/Apis/Odrc/WaardelijstResponseModel.cs @@ -1,13 +1,8 @@ -using System.ComponentModel; -using System.Text.Json.Serialization; - -namespace ODPC.Apis.Odrc +namespace ODPC.Apis.Odrc { public class WaardelijstResponseModel - { - [JsonPropertyName("uuid")] - public required string Id { get; set; } - [JsonPropertyName("naam")] - public required string Name { get; set; } + { + public required string Uuid { get; set; } + public required string Naam { get; set; } } } diff --git a/ODPC.Server/Authentication/AuthenticationExtensions.cs b/ODPC.Server/Authentication/AuthenticationExtensions.cs index 9820353..3064b41 100644 --- a/ODPC.Server/Authentication/AuthenticationExtensions.cs +++ b/ODPC.Server/Authentication/AuthenticationExtensions.cs @@ -22,14 +22,14 @@ public static void AddAuth(this IServiceCollection services, Action services.AddHttpContextAccessor(); - services.AddScoped(s => + services.AddScoped(s => { var user = s.GetRequiredService().HttpContext?.User; var isLoggedIn = user?.Identity?.IsAuthenticated ?? false; var name = user?.FindFirst(nameClaimType)?.Value; var id = user?.FindFirst(x => idClaimTypes.Contains(x.Type))?.Value; var roles = user?.FindAll(roleClaimType).Select(x=> x.Value).ToArray() ?? []; - return new OdpUser { IsLoggedIn = isLoggedIn, FullName = name, Id = id, Roles = roles }; + return new OdpcUser { IsLoggedIn = isLoggedIn, FullName = name, Id = id, Roles = roles }; }); var authBuilder = services.AddAuthentication(options => @@ -107,7 +107,7 @@ public static void AddAuth(this IServiceCollection services, Action public static IEndpointRouteBuilder MapOdpcAuthEndpoints(this IEndpointRouteBuilder endpoints) { endpoints.MapGet("api/logoff", LogoffAsync).AllowAnonymous(); - endpoints.MapGet("api/me", (OdpUser user) => user).AllowAnonymous(); + endpoints.MapGet("api/me", (OdpcUser user) => user).AllowAnonymous(); endpoints.MapGet("api/challenge", ChallengeAsync).AllowAnonymous(); return endpoints; diff --git a/ODPC.Server/Authentication/OdpUser.cs b/ODPC.Server/Authentication/OdpcUser.cs similarity index 90% rename from ODPC.Server/Authentication/OdpUser.cs rename to ODPC.Server/Authentication/OdpcUser.cs index 6487b11..a6b12f5 100644 --- a/ODPC.Server/Authentication/OdpUser.cs +++ b/ODPC.Server/Authentication/OdpcUser.cs @@ -1,6 +1,6 @@ namespace ODPC.Authentication { - public record OdpUser + public record OdpcUser { public required bool IsLoggedIn { get; init; } public required string? Id { get; init; } diff --git a/ODPC.Server/Features/GebruikerWaardelijstItemsService.cs b/ODPC.Server/Features/GebruikerWaardelijstItemsService.cs new file mode 100644 index 0000000..40967c2 --- /dev/null +++ b/ODPC.Server/Features/GebruikerWaardelijstItemsService.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using ODPC.Authentication; +using ODPC.Data; + +namespace ODPC.Features +{ + public interface IGebruikerWaardelijstItemsService + { + Task> GetAsync(CancellationToken token); + } + + public class GebruikerWaardelijstItemsService(OdpcUser user, OdpcDbContext context) : IGebruikerWaardelijstItemsService + { + public async Task> GetAsync(CancellationToken token) + { + var groepIds = context.GebruikersgroepGebruikers + .Where(x => x.GebruikerId == user.Id) + .Select(x => x.GebruikersgroepUuid); + + return await context.GebruikersgroepWaardelijsten + .Where(x => groepIds.Contains(x.GebruikersgroepUuid)) + .Select(x => x.WaardelijstId) + .Distinct() + .ToListAsync(token); + } + } +} diff --git a/ODPC.Server/Features/Informatiecategorieen/AlleInformatiecategorieen/InformatiecategorieenController.cs b/ODPC.Server/Features/Informatiecategorieen/AlleInformatiecategorieen/InformatiecategorieenController.cs new file mode 100644 index 0000000..3085a03 --- /dev/null +++ b/ODPC.Server/Features/Informatiecategorieen/AlleInformatiecategorieen/InformatiecategorieenController.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using ODPC.Apis.Odrc; + +namespace ODPC.Features.Informatiecategorieen.AlleInformatiecategorieen +{ + [ApiController] + public class InformatiecategorieenController(IOdrcClientFactory clientFactory) : ControllerBase + { + [HttpGet("api/v1/informatiecategorieen")] + public async Task Get([FromQuery] string? page, CancellationToken token) + { + //infocategorien ophalen uit het ODRC + using var client = clientFactory.Create("Alle informatiecategorieen ophalen"); + var json = await client.GetFromJsonAsync>("/api/v1/informatiecategorieen?page=" + page, token); + if (json != null) + { + json.Previous = GetPathAndQuery(json.Previous); + json.Next = GetPathAndQuery(json.Next); + } + return Ok(json); + } + + private static string? GetPathAndQuery(string? url) => Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri) + ? uri.PathAndQuery + : url; + } +} diff --git a/ODPC.Server/Features/Informatiecategorieen/MijnInformatiecategorieen/MijnInformatiecategorieenController.cs b/ODPC.Server/Features/Informatiecategorieen/MijnInformatiecategorieen/MijnInformatiecategorieenController.cs new file mode 100644 index 0000000..d4a11d7 --- /dev/null +++ b/ODPC.Server/Features/Informatiecategorieen/MijnInformatiecategorieen/MijnInformatiecategorieenController.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ODPC.Apis.Odrc; + +namespace ODPC.Features.Informatiecategorieen.MijnInformatiecategorieen +{ + [ApiController] + public class MijnInformatiecategorieenController(IOdrcClientFactory clientFactory, IGebruikerWaardelijstItemsService waardelijstItemsService) : ControllerBase + { + [HttpGet("api/v1/mijn-informatiecategorieen")] + public async IAsyncEnumerable Get([EnumeratorCancellation] CancellationToken token) + { + var categorieen = await waardelijstItemsService.GetAsync(token); + + if (categorieen.Count == 0) yield break; + + using var client = clientFactory.Create("Eigen informatiecategorieen ophalen"); + var url = "api/v1/informatiecategorieen"; + + // omdat we zelf moeten filteren obv van de waardelijstitems waar de gebruiker toegang toe heeft, + // kunnen we geen paginering gebruiker. we lopen door alle pagina's van de ODRC + while (!string.IsNullOrWhiteSpace(url)) + { + var page = await client.GetFromJsonAsync>(url, token) ?? new() { Results = [] }; + foreach (var item in page.Results) + { + if (item["uuid"]?.GetValue() is string uuid && categorieen.Contains(uuid)) + { + yield return item; + } + } + url = page?.Next; + } + } + } +} diff --git a/ODPC.Server/Features/Organisaties/AlleOrganisaties/OrganisatiesController.cs b/ODPC.Server/Features/Organisaties/AlleOrganisaties/OrganisatiesController.cs new file mode 100644 index 0000000..dd75bc0 --- /dev/null +++ b/ODPC.Server/Features/Organisaties/AlleOrganisaties/OrganisatiesController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using ODPC.Apis.Odrc; + +namespace ODPC.Features.Organisaties.AlleOrganisaties +{ + [ApiController] + public class OrganisatiesController : ControllerBase + { + [HttpGet("api/v1/organisaties")] + public IActionResult Get([FromQuery] string? page) => Ok(new PagedResponseModel + { + Results = OrganisatiesMock.Organisaties.Values.ToList(), + Count = OrganisatiesMock.Organisaties.Count, + }); + } +} diff --git a/ODPC.Server/Features/Organisaties/OrganisatiesMock.cs b/ODPC.Server/Features/Organisaties/OrganisatiesMock.cs new file mode 100644 index 0000000..81e7e86 --- /dev/null +++ b/ODPC.Server/Features/Organisaties/OrganisatiesMock.cs @@ -0,0 +1,13 @@ +using ODPC.Apis.Odrc; + +namespace ODPC.Features.Organisaties +{ + public class OrganisatiesMock + { + public static readonly Dictionary Organisaties = new WaardelijstResponseModel[] + { + new() { Uuid = "8f939b51-dad3-436d-a5fa-495b42317d64", Naam = "Organisatie 2" }, + new() { Uuid = "5c14e7e2-00a2-4990-adbb-7290cd89fb6e", Naam = "Organisatie 3" } + }.ToDictionary(x => x.Uuid); + } +} diff --git a/ODPC.Server/Features/Publicaties/Publicatie.cs b/ODPC.Server/Features/Publicaties/Publicatie.cs index 380795a..c77f60f 100644 --- a/ODPC.Server/Features/Publicaties/Publicatie.cs +++ b/ODPC.Server/Features/Publicaties/Publicatie.cs @@ -7,6 +7,7 @@ public class Publicatie public string? VerkorteTitel { get; set; } public string? Omschrijving { get; set; } public DateTime Registratiedatum { get; set; } - public string Status { get; set; } + public string? Status { get; set; } + public List? GekoppeldeInformatiecategorieen { get; set; } } } diff --git a/ODPC.Server/Features/Publicaties/PublicatieBijwerken/PublicatieBijwerkenController.cs b/ODPC.Server/Features/Publicaties/PublicatieBijwerken/PublicatieBijwerkenController.cs index d05081d..7c68d2f 100644 --- a/ODPC.Server/Features/Publicaties/PublicatieBijwerken/PublicatieBijwerkenController.cs +++ b/ODPC.Server/Features/Publicaties/PublicatieBijwerken/PublicatieBijwerkenController.cs @@ -3,11 +3,19 @@ namespace ODPC.Features.Publicaties.PublicatieBijwerken { [ApiController] - public class PublicatieBijwerkenController : ControllerBase + public class PublicatieBijwerkenController(IGebruikerWaardelijstItemsService waardelijstItemsService) : ControllerBase { [HttpPut("api/v1/publicaties/{uuid}")] - public IActionResult Put(Guid uuid, Publicatie publicatie) + public async Task Put(Guid uuid, Publicatie publicatie, CancellationToken token) { + var categorieen = await waardelijstItemsService.GetAsync(token); + + if (publicatie.GekoppeldeInformatiecategorieen != null && publicatie.GekoppeldeInformatiecategorieen.Any(c => !categorieen.Contains(c))) + { + ModelState.AddModelError(nameof(publicatie.GekoppeldeInformatiecategorieen), "Gebruiker is niet geautoriseerd voor deze informatiecategorieën"); + return BadRequest(ModelState); + } + PublicatiesMock.Publicaties[uuid] = publicatie; return Ok(publicatie); } diff --git a/ODPC.Server/Features/Publicaties/PublicatieRegistreren/PublicatieRegistrerenController.cs b/ODPC.Server/Features/Publicaties/PublicatieRegistreren/PublicatieRegistrerenController.cs index c9b78af..525d930 100644 --- a/ODPC.Server/Features/Publicaties/PublicatieRegistreren/PublicatieRegistrerenController.cs +++ b/ODPC.Server/Features/Publicaties/PublicatieRegistreren/PublicatieRegistrerenController.cs @@ -1,37 +1,40 @@ -using System.Text.Json; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using ODPC.Apis.Odrc; -using ODPC.Authentication; -using ODPC.Config; namespace ODPC.Features.Publicaties.PublicatieRegistreren { [ApiController] - public class PublicatieRegistrerenController(OdpUser user, IOdrcClientFactory clientFactory) : ControllerBase + public class PublicatieRegistrerenController(IOdrcClientFactory clientFactory, IGebruikerWaardelijstItemsService waardelijstItemsService) : ControllerBase { private readonly IOdrcClientFactory _clientFactory = clientFactory; [HttpPost("api/v1/publicaties")] - public async Task Post(Publicatie publicatie) + public async Task Post(Publicatie publicatie, CancellationToken token) { - publicatie.Uuid = Guid.NewGuid(); - publicatie.Registratiedatum = DateTime.Now; + var categorieen = await waardelijstItemsService.GetAsync(token); - //de mock laten we er nog even in totdat ook het ophalen van gegevens via het register verloopt - PublicatiesMock.Publicaties[publicatie.Uuid] = publicatie; + if (publicatie.GekoppeldeInformatiecategorieen != null && publicatie.GekoppeldeInformatiecategorieen.Any(c => !categorieen.Contains(c))) + { + ModelState.AddModelError(nameof(publicatie.GekoppeldeInformatiecategorieen), "Gebruiker is niet geautoriseerd voor deze informatiecategorieën"); + return BadRequest(ModelState); + } - var client = _clientFactory.Create(user, "Publicatie geregistreerd"); + var client = _clientFactory.Create("Publicatie geregistreerd"); + var response = await client.PostAsJsonAsync("/api/v1/publicaties", publicatie, token); - var response = await client.PostAsJsonAsync("/api/v1/publicaties", publicatie, new CancellationToken()); - response.EnsureSuccessStatusCode(); - - var responseBody = await response.Content.ReadAsStringAsync(); + var viewModel = await response.Content.ReadFromJsonAsync(token); - var viewModel = JsonSerializer.Deserialize(responseBody, JsonSerialization.Options); + // TODO deze regel kan eraf als deze story is geimplementeerd: https://github.com/GeneriekPublicatiePlatformWoo/registratie-component/issues/48 + viewModel!.GekoppeldeInformatiecategorieen = publicatie.GekoppeldeInformatiecategorieen; + + // TODO deze regel kan eraf als deze story is geimplementeerd: https://github.com/GeneriekPublicatiePlatformWoo/registratie-component/issues/49 + viewModel!.Status = publicatie.Status; + + // TODO de mock laten we er nog even in totdat ook het ophalen van gegevens via het register verloopt: https://github.com/GeneriekPublicatiePlatformWoo/Openbaar-Documenten-Publicatie-Component/issues/68 + PublicatiesMock.Publicaties[viewModel!.Uuid] = viewModel; return Ok(viewModel); } - } } diff --git a/ODPC.Server/Features/Publicaties/PublicatiesMock.cs b/ODPC.Server/Features/Publicaties/PublicatiesMock.cs index 4c1a34b..3347e50 100644 --- a/ODPC.Server/Features/Publicaties/PublicatiesMock.cs +++ b/ODPC.Server/Features/Publicaties/PublicatiesMock.cs @@ -11,7 +11,9 @@ public static class PublicatiesMock VerkorteTitel = "De Impact van de Wet open overheid op Bestuurlijke Transparantie", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 24), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] + }, new() { @@ -20,7 +22,8 @@ public static class PublicatiesMock VerkorteTitel = "Toepassing en Resultaten van de Wet open overheid", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 23), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -29,7 +32,8 @@ public static class PublicatiesMock VerkorteTitel = "Transparantie als Standaard in Bestuurlijk Nederland", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 03), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -38,7 +42,8 @@ public static class PublicatiesMock VerkorteTitel = "Een Nieuwe Norm voor Openbare Informatie", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 02), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -47,7 +52,8 @@ public static class PublicatiesMock VerkorteTitel = "De Toekomst van Transparantie met de Woo", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 29), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -56,7 +62,8 @@ public static class PublicatiesMock VerkorteTitel = "De Praktische Uitwerking van de Woo", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 07), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -65,7 +72,8 @@ public static class PublicatiesMock VerkorteTitel = "Openbaarheid en Verantwoording", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 27), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -74,7 +82,8 @@ public static class PublicatiesMock VerkorteTitel = "Hoe de Woo de Overheid Hervormt", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 07), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -83,7 +92,8 @@ public static class PublicatiesMock VerkorteTitel = "Successen en Uitdagingen", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 14), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -92,7 +102,8 @@ public static class PublicatiesMock VerkorteTitel = "Het Effect van de Woo", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 23), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -101,7 +112,8 @@ public static class PublicatiesMock VerkorteTitel = "Verantwoord Bestuur door de Wet open overheid", Omschrijving = "", Registratiedatum = new DateTime(2024, 08, 09), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] }, new() { @@ -110,7 +122,8 @@ public static class PublicatiesMock VerkorteTitel = "De Woo en de Weg naar Open Overheid", Omschrijving = "", Registratiedatum = new DateTime(2024, 05, 15), - Status = "gepubliceerd" + Status = "gepubliceerd", + GekoppeldeInformatiecategorieen = [] } }.ToDictionary(x => x.Uuid); } diff --git a/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstViewModel.cs b/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstViewModel.cs deleted file mode 100644 index 8778934..0000000 --- a/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel; -using System.Text.Json.Serialization; - -namespace ODPC.Features.WaardeLijstenOverzicht -{ - public class WaardelijstViewModel - { - - public required string Id { get; set; } - - public required string Name { get; set; } - - public string Type { get; set; } = "INFORMATIECATEGORIE"; //tijdelijk. als de andere categorien ook uit het odrc gehaald worden, zullen de waardelijsten volledig gesplitst worden - } -} diff --git a/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstenController.cs b/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstenController.cs deleted file mode 100644 index 6390570..0000000 --- a/ODPC.Server/Features/WaardeLijstenOverzicht/WaardelijstenController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text.Json; -using System.Xml.Linq; -using Microsoft.AspNetCore.Mvc; -using ODPC.Apis.Odrc; -using ODPC.Authentication; -using ODPC.Config; -using ODPC.Features.Publicaties; - -namespace ODPC.Features.WaardeLijstenOverzicht -{ - [Route("api/v1/[controller]")] - [ApiController] - public class WaardelijstenController(OdpUser user, IOdrcClientFactory clientFactory) : ControllerBase - { - [HttpGet] - public async Task> GetAsync() - { - //infocategorien ophalen uit het ODRC - - //user en audit log handeling meegeven is misschien nog niet nodig, maar doet geen kwaad - var client = clientFactory.Create(user, "Waardelijsten ophalen"); - - var response = await client.GetAsync("/api/v1/informatiecategorieen", new CancellationToken()); - - response.EnsureSuccessStatusCode(); - - var responseBody = await response.Content.ReadAsStringAsync(); - - var deserializedResponse = JsonSerializer.Deserialize>(responseBody, JsonSerialization.Options); - - var informatiecategorieen = deserializedResponse?.results ?? []; - - var waardelijstenViewModel = informatiecategorieen.Select(x=> new WaardelijstViewModel { Id = x.Id, Name = x.Name}).ToList(); - - - //nog wat dummy data toevoegen van andersoortige waardelijsten - waardelijstenViewModel.Add(new WaardelijstViewModel() { Id = "d3da5277-ea07-4921-97b8-e9a181390c76", Name = "arbeidsomstandigheden", Type = "THEMA" }); - waardelijstenViewModel.Add(new WaardelijstViewModel() { Id = "066241fe-7f39-41da-8efb-a702ef32b7d0", Name = "afval", Type = "THEMA" }); - waardelijstenViewModel.Add(new WaardelijstViewModel() { Id = "8f939b51-dad3-436d-a5fa-495b42317d64", Name = "Organisatie 2", Type="ORGANISATIE" }); - waardelijstenViewModel.Add(new WaardelijstViewModel() { Id = "5c14e7e2-00a2-4990-adbb-7290cd89fb6e", Name = "Organisatie 3", Type = "ORGANISATIE" }); - waardelijstenViewModel.Add(new WaardelijstViewModel() { Id = "0e7a0023-423a-421a-8700-359232fef584", Name = "europese zaken", Type = "THEMA" }); - - return waardelijstenViewModel; - } - - } -} diff --git a/ODPC.Server/Program.cs b/ODPC.Server/Program.cs index 6da69d7..4202390 100644 --- a/ODPC.Server/Program.cs +++ b/ODPC.Server/Program.cs @@ -2,6 +2,7 @@ using ODPC.Apis.Odrc; using ODPC.Authentication; using ODPC.Data; +using ODPC.Features; using Serilog; using Serilog.Events; using Serilog.Formatting.Json; @@ -41,7 +42,7 @@ var connStr = $"Username={builder.Configuration["POSTGRES_USER"]};Password={builder.Configuration["POSTGRES_PASSWORD"]};Host={builder.Configuration["POSTGRES_HOST"]};Database={builder.Configuration["POSTGRES_DB"]};Port={builder.Configuration["POSTGRES_PORT"]}"; builder.Services.AddDbContext(opt => opt.UseNpgsql(connStr)); builder.Services.AddScoped(); - + builder.Services.AddScoped(); var app = builder.Build(); diff --git a/odpc.client/src/api/fetch-logged-in.ts b/odpc.client/src/api/fetch-logged-in.ts deleted file mode 100644 index a389eac..0000000 --- a/odpc.client/src/api/fetch-logged-in.ts +++ /dev/null @@ -1,72 +0,0 @@ -type FetchArgs = Parameters; -type FetchReturn = ReturnType; - -// eslint-disable-next-line @typescript-eslint/no-empty-function -const empty = () => {}; - -const waitForLogin = { - promise: Promise.resolve(), - resolve: empty -}; - -(function refreshPromise() { - const promise = new Promise((resolve) => { - waitForLogin.resolve = resolve; - }); - - waitForLogin.promise = promise; - - // will keep refreshing the promise whenever it resolves, - // which is done when succesfully logged in. - // this causes all pending 401 requests to retry, - // but new 401 requests to wait for the new promise. - promise.finally(refreshPromise); -})(); - -export function handleLogin() { - waitForLogin.resolve(); -} - -export function setHeader(init: RequestInit, key: string, value: string) { - if (!init.headers) { - init.headers = {}; - } - - if (Array.isArray(init.headers)) { - init.headers.push([key, value]); - } else if (init.headers instanceof Headers) { - init.headers.set(key, value); - } else { - init.headers[key] = value; - } -} - -export async function fetchLoggedIn(...args: FetchArgs): FetchReturn { - const init = args[1] || {}; - args[1] = init; - - setHeader(init, "is-api", "true"); - - const r = await fetch(...args); - - if (r.status === 401) { - console.warn("session expired. waiting for user to log in..."); - - return waitForLogin.promise.then(() => { - console.log("user is logged in again, resuming..."); - - return fetchLoggedIn(...args); - }); - } - - return r; -} - -export function throwIfNotOk(response: Response) { - if (!response.ok) throw new Error(response.statusText); - return response as Response & { ok: true }; -} - -export function parseJson(response: Response) { - return response.json(); -} diff --git a/odpc.client/src/api/index.ts b/odpc.client/src/api/index.ts new file mode 100644 index 0000000..1e3c9bb --- /dev/null +++ b/odpc.client/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./use-fetch-api"; +export * from "./types"; diff --git a/odpc.client/src/api/types.ts b/odpc.client/src/api/types.ts new file mode 100644 index 0000000..269b500 --- /dev/null +++ b/odpc.client/src/api/types.ts @@ -0,0 +1,6 @@ +export type PagedResult = Readonly<{ + count: number; + next?: string; + previous?: string; + results: T[]; +}>; diff --git a/odpc.client/src/assets/main.scss b/odpc.client/src/assets/main.scss index cc52f9d..c18d858 100644 --- a/odpc.client/src/assets/main.scss +++ b/odpc.client/src/assets/main.scss @@ -80,7 +80,7 @@ body { color: var(--code); } - &:invalid.invalid { + &:user-invalid { border-color: var(--code); outline-color: var(--code); @@ -91,6 +91,12 @@ body { } } +details { + summary { + word-break: break-word; + } +} + .form-group-button { display: grid; grid-template-columns: 1fr auto; @@ -111,6 +117,20 @@ body { } } +.checkbox { + margin-block-end: var(--spacing-small); + + label { + display: flex; + align-items: flex-start; + column-gap: var(--spacing-small); + } + + input { + margin-block: 1px; + } +} + .reset { list-style: none; padding: 0; diff --git a/odpc.client/src/components/CheckboxList.vue b/odpc.client/src/components/CheckboxList.vue deleted file mode 100644 index 299f793..0000000 --- a/odpc.client/src/components/CheckboxList.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/odpc.client/src/components/checkbox-group/CheckboxGroup.vue b/odpc.client/src/components/checkbox-group/CheckboxGroup.vue new file mode 100644 index 0000000..68ab32f --- /dev/null +++ b/odpc.client/src/components/checkbox-group/CheckboxGroup.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/odpc.client/src/components/checkbox-group/types.ts b/odpc.client/src/components/checkbox-group/types.ts new file mode 100644 index 0000000..e9fda29 --- /dev/null +++ b/odpc.client/src/components/checkbox-group/types.ts @@ -0,0 +1,4 @@ +export type OptionProps = { + uuid: string; + naam: string; +}; diff --git a/odpc.client/src/components/checkbox-group/use-checkbox-group.ts b/odpc.client/src/components/checkbox-group/use-checkbox-group.ts new file mode 100644 index 0000000..c03b419 --- /dev/null +++ b/odpc.client/src/components/checkbox-group/use-checkbox-group.ts @@ -0,0 +1,28 @@ +import { ref, watchEffect } from "vue"; + +export const useCheckboxGroup = () => { + const groupRef = ref(); + + const isAnyChecked = (checkboxes: NodeListOf) => + Array.from(checkboxes).some((checkbox) => checkbox.checked); + + const setCustomValidity = () => { + const checkboxes = (groupRef.value?.querySelectorAll("[type='checkbox']") || + []) as NodeListOf; + + checkboxes.forEach((checkbox) => + checkbox.setCustomValidity(!isAnyChecked(checkboxes) ? "Kies minimaal één optie." : "") + ); + }; + + const onInvalid = () => + groupRef.value instanceof HTMLDetailsElement && (groupRef.value.open = true); + + watchEffect(() => groupRef.value?.hasAttribute("aria-required") && setCustomValidity()); + + return { + groupRef, + setCustomValidity, + onInvalid + }; +}; diff --git a/odpc.client/src/composables/use-all-pages.ts b/odpc.client/src/composables/use-all-pages.ts new file mode 100644 index 0000000..68849de --- /dev/null +++ b/odpc.client/src/composables/use-all-pages.ts @@ -0,0 +1,38 @@ +import type { PagedResult } from "@/api"; +import { asyncComputed } from "@vueuse/core"; +import { ref } from "vue"; + +const fetchPage = (url: string, signal?: AbortSignal | undefined) => + fetch(url, { headers: { "is-api": "true" }, signal }) + .then((r) => (r.ok ? r : Promise.reject(r))) + .then((r) => r.json() as Promise>); + +const fetchAllPages = async (url: string, signal?: AbortSignal | undefined): Promise => { + const { results, next } = await fetchPage(url, signal); + if (next) return [...results, ...(await fetchAllPages(next, signal))]; + return results; +}; + +export const useAllPages = (url: string) => { + const error = ref(false); + const loading = ref(true); + const data = asyncComputed( + (onCancel) => { + const abortController = new AbortController(); + + onCancel(() => abortController.abort()); + + return fetchAllPages(url, abortController.signal).catch(() => { + error.value = true; + return [] as T[]; + }); + }, + [], + loading + ); + return { + error, + loading, + data + }; +}; diff --git a/odpc.client/src/composables/use-paged-search.ts b/odpc.client/src/composables/use-paged-search.ts index d4e84c6..9488540 100644 --- a/odpc.client/src/composables/use-paged-search.ts +++ b/odpc.client/src/composables/use-paged-search.ts @@ -1,18 +1,11 @@ import { ref, watch, computed, onMounted, type Ref } from "vue"; import { useRouter } from "vue-router"; import { useUrlSearchParams } from "@vueuse/core"; -import { useFetchApi } from "@/api/use-fetch-api"; +import { useFetchApi, type PagedResult } from "@/api"; const API_URL = `/api/v1`; const PAGE_SIZE = 5; -type PagedResult = { - count: number; - next?: string; - previous?: string; - results: T[]; -}; - export const usePagedSearch = ( endpoint: string, params: QueryParams diff --git a/odpc.client/src/features/gebruikersgroep/GebruikersgroepDetails.vue b/odpc.client/src/features/gebruikersgroep/GebruikersgroepDetails.vue index 51ec387..98bc4cc 100644 --- a/odpc.client/src/features/gebruikersgroep/GebruikersgroepDetails.vue +++ b/odpc.client/src/features/gebruikersgroep/GebruikersgroepDetails.vue @@ -1,24 +1,15 @@