Convert to Blazor #6
@ -3,5 +3,9 @@
 | 
				
			|||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <RazorPage_SelectedScaffolderID>RazorPageScaffolder</RazorPage_SelectedScaffolderID>
 | 
					    <RazorPage_SelectedScaffolderID>RazorPageScaffolder</RazorPage_SelectedScaffolderID>
 | 
				
			||||||
    <RazorPage_SelectedScaffolderCategoryPath>root/Common/RazorPage</RazorPage_SelectedScaffolderCategoryPath>
 | 
					    <RazorPage_SelectedScaffolderCategoryPath>root/Common/RazorPage</RazorPage_SelectedScaffolderCategoryPath>
 | 
				
			||||||
 | 
					    <ActiveDebugProfile>JobsJobsJobs</ActiveDebugProfile>
 | 
				
			||||||
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
 | 
				
			||||||
 | 
					    <DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
@ -1,3 +1,36 @@
 | 
				
			|||||||
@page "/citizen/authorized"
 | 
					@page "/citizen/authorized"
 | 
				
			||||||
 | 
					@inject HttpClient http
 | 
				
			||||||
 | 
					@inject NavigationManager nav
 | 
				
			||||||
 | 
					@inject AppState state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p>@Message</p>
 | 
					<p>@message</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@code {
 | 
				
			||||||
 | 
					    string message = "Logging you on with No Agenda Social...";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected override async Task OnInitializedAsync()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      // Exchange authorization code for a JWT
 | 
				
			||||||
 | 
					      var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query);
 | 
				
			||||||
 | 
					      if (query.TryGetValue("code", out var authCode))
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        var logOnResult = await ServerApi.LogOn(http, authCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (logOnResult.IsOk)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          var logOn = logOnResult.Ok;
 | 
				
			||||||
 | 
					          state.User = new UserInfo(logOn.CitizenId, logOn.Name);
 | 
				
			||||||
 | 
					          state.Jwt = logOn.Jwt;
 | 
				
			||||||
 | 
					          nav.NavigateTo("/citizen/dashboard");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          message = logOnResult.Error;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        message = "Did not receive a token from No Agenda Social (perhaps you clicked \"Cancel\"?)";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,69 +0,0 @@
 | 
				
			|||||||
using JobsJobsJobs.Shared.Api;
 | 
					 | 
				
			||||||
using Microsoft.AspNetCore.Components;
 | 
					 | 
				
			||||||
using Microsoft.AspNetCore.WebUtilities;
 | 
					 | 
				
			||||||
using System.Net.Http;
 | 
					 | 
				
			||||||
using System.Net.Http.Json;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace JobsJobsJobs.Client.Pages.Citizen
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    public partial class Authorized : ComponentBase
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// The message to be displayed to the user
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        public string Message { get; set; } = "Logging you on with No Agenda Social...";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// HTTP client for performing API calls
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        [Inject]
 | 
					 | 
				
			||||||
        public HttpClient Http { get; init; } = default!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Navigation manager for getting parameters from the URL
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        [Inject]
 | 
					 | 
				
			||||||
        public NavigationManager Navigation { get; set; } = default!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /// <summary>
 | 
					 | 
				
			||||||
        /// Application state
 | 
					 | 
				
			||||||
        /// </summary>
 | 
					 | 
				
			||||||
        [Inject]
 | 
					 | 
				
			||||||
        public AppState State { get; set; } = default!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        protected override async Task OnInitializedAsync()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // Exchange authorization code for a JWT
 | 
					 | 
				
			||||||
            var query = QueryHelpers.ParseQuery(Navigation.ToAbsoluteUri(Navigation.Uri).Query);
 | 
					 | 
				
			||||||
            if (query.TryGetValue("code", out var authCode))
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var logOnResult = await Http.GetAsync($"api/citizen/log-on/{authCode}");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (logOnResult != null)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    if (logOnResult.IsSuccessStatusCode)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        var logOn = (await logOnResult.Content.ReadFromJsonAsync<LogOnSuccess>())!;
 | 
					 | 
				
			||||||
                        State.User = new UserInfo(logOn.CitizenId, logOn.Name);
 | 
					 | 
				
			||||||
                        State.Jwt = logOn.Jwt;
 | 
					 | 
				
			||||||
                        Navigation.NavigateTo("/citizen/dashboard");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        var errorMessage = await logOnResult.Content.ReadAsStringAsync();
 | 
					 | 
				
			||||||
                        Message = $"Unable to log on with No Agenda Social: {errorMessage}";
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Message = "Unable to log on with No Agenda Social. This should never happen; contact @danieljsummers";
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Message = "Did not receive a token from No Agenda Social (perhaps you clicked \"Cancel\"?)";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,9 +1,38 @@
 | 
				
			|||||||
@page "/citizen/dashboard"
 | 
					@page "/citizen/dashboard"
 | 
				
			||||||
 | 
					@inject HttpClient http
 | 
				
			||||||
 | 
					@inject AppState state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h3>Dashboard</h3>
 | 
					<h3>Dashboard</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<p>Here's the dashboard, homie...</p>
 | 
					<p>Here's the dashboard, homie...</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@if (hasProfile != null)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  <p>Has profile? @hasProfile</p>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@if (errorMessage != null)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  <p>@errorMessage</p>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@code {
 | 
					@code {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool? hasProfile = null;
 | 
				
			||||||
 | 
					    string? errorMessage = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected override async Task OnInitializedAsync()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      if (state.User != null)
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        var profile = await ServerApi.RetrieveProfile(http, state);
 | 
				
			||||||
 | 
					        if (profile.IsOk)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          hasProfile = profile.Ok != null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          errorMessage = profile.Error;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										81
									
								
								src/JobsJobsJobs/Client/ServerApi.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/JobsJobsJobs/Client/ServerApi.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					using JobsJobsJobs.Shared;
 | 
				
			||||||
 | 
					using JobsJobsJobs.Shared.Api;
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Net;
 | 
				
			||||||
 | 
					using System.Net.Http;
 | 
				
			||||||
 | 
					using System.Net.Http.Headers;
 | 
				
			||||||
 | 
					using System.Net.Http.Json;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace JobsJobsJobs.Client
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Functions used to access the API
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public static class ServerApi
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Create an API URL
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="url">The URL to append to the API base URL</param>
 | 
				
			||||||
 | 
					        /// <returns>The full URL to be used in HTTP requests</returns>
 | 
				
			||||||
 | 
					        private static string ApiUrl(string url) => $"/api/{url}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Create an HTTP request with an authorization header
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="state">The current application state</param>
 | 
				
			||||||
 | 
					        /// <param name="url">The URL for the request (will be appended to the API root)</param>
 | 
				
			||||||
 | 
					        /// <param name="method">The request method (optional, defaults to GET)</param>
 | 
				
			||||||
 | 
					        /// <returns>A request with the header attached, ready for further manipulation</returns>
 | 
				
			||||||
 | 
					        private static HttpRequestMessage WithHeader(AppState state, string url, HttpMethod? method = null)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var req = new HttpRequestMessage(method ?? HttpMethod.Get, ApiUrl(url));
 | 
				
			||||||
 | 
					            req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", state.Jwt);
 | 
				
			||||||
 | 
					            return req;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Log on a user with the authorization code received from No Agenda Social
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="http">The HTTP client to use for server communication</param>
 | 
				
			||||||
 | 
					        /// <param name="authCode">The authorization code received from NAS</param>
 | 
				
			||||||
 | 
					        /// <returns>The log on details if successful, an error if not</returns>
 | 
				
			||||||
 | 
					        public static async Task<Result<LogOnSuccess>> LogOn(HttpClient http, string authCode)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            try
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var logOn = await http.GetFromJsonAsync<LogOnSuccess>(ApiUrl($"citizen/log-on/{authCode}"));
 | 
				
			||||||
 | 
					                if (logOn == null) {
 | 
				
			||||||
 | 
					                    return Result<LogOnSuccess>.AsError(
 | 
				
			||||||
 | 
					                        "Unable to log on with No Agenda Social. This should never happen; contact @danieljsummers");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return Result<LogOnSuccess>.AsOk(logOn);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            catch (HttpRequestException ex)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return Result<LogOnSuccess>.AsError($"Unable to log on with No Agenda Social: {ex.Message}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Retrieve a citizen's profile
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="http">The HTTP client to use for server communication</param>
 | 
				
			||||||
 | 
					        /// <param name="state">The current application state</param>
 | 
				
			||||||
 | 
					        /// <returns>The citizen's profile, null if it is not found, or an error message if one occurs</returns>
 | 
				
			||||||
 | 
					        public static async Task<Result<Profile?>> RetrieveProfile(HttpClient http, AppState state)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var req = WithHeader(state, "profile/");
 | 
				
			||||||
 | 
					            var res = await http.SendAsync(req);
 | 
				
			||||||
 | 
					            return true switch
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _ when res.StatusCode == HttpStatusCode.NoContent => Result<Profile?>.AsOk(null),
 | 
				
			||||||
 | 
					                _ when res.IsSuccessStatusCode => Result<Profile?>.AsOk(await res.Content.ReadFromJsonAsync<Profile>()),
 | 
				
			||||||
 | 
					                _ => Result<Profile?>.AsError(await res.Content.ReadAsStringAsync()),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -9,3 +9,4 @@
 | 
				
			|||||||
@using Microsoft.JSInterop
 | 
					@using Microsoft.JSInterop
 | 
				
			||||||
@using JobsJobsJobs.Client
 | 
					@using JobsJobsJobs.Client
 | 
				
			||||||
@using JobsJobsJobs.Client.Shared
 | 
					@using JobsJobsJobs.Client.Shared
 | 
				
			||||||
 | 
					@using JobsJobsJobs.Shared
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,14 @@
 | 
				
			|||||||
using Microsoft.AspNetCore.Http;
 | 
					using JobsJobsJobs.Server.Data;
 | 
				
			||||||
 | 
					using JobsJobsJobs.Shared;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Authorization;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
using Microsoft.AspNetCore.Mvc;
 | 
					using Microsoft.AspNetCore.Mvc;
 | 
				
			||||||
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
 | 
					using Npgsql;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Security.Claims;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
 | 
					namespace JobsJobsJobs.Server.Areas.Api.Controllers
 | 
				
			||||||
@ -11,10 +17,24 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
 | 
				
			|||||||
    [ApiController]
 | 
					    [ApiController]
 | 
				
			||||||
    public class ProfileController : ControllerBase
 | 
					    public class ProfileController : ControllerBase
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [HttpGet]
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// The database connection
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        private readonly NpgsqlConnection db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public ProfileController(NpgsqlConnection dbConn)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            db = dbConn;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [Authorize]
 | 
				
			||||||
 | 
					        [HttpGet("")]
 | 
				
			||||||
        public async Task<IActionResult> Get()
 | 
					        public async Task<IActionResult> Get()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return null;
 | 
					            await db.OpenAsync();
 | 
				
			||||||
 | 
					            var profile = await db.FindProfileByCitizen(
 | 
				
			||||||
 | 
					                CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value));
 | 
				
			||||||
 | 
					            return profile == null ? NoContent() : Ok(profile);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,11 +13,11 @@ namespace JobsJobsJobs.Server.Data
 | 
				
			|||||||
    public static class NpgsqlDataReaderExtensions
 | 
					    public static class NpgsqlDataReaderExtensions
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Get a string by its name
 | 
					        /// Get a boolean by its name
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <param name="name">The name of the field to be retrieved as a string</param>
 | 
					        /// <param name="name">The name of the field to be retrieved as a boolean</param>
 | 
				
			||||||
        /// <returns>The specified field as a string</returns>
 | 
					        /// <returns>The specified field as a boolean</returns>
 | 
				
			||||||
        public static string GetString(this NpgsqlDataReader rdr, string name) => rdr.GetString(rdr.GetOrdinal(name));
 | 
					        public static bool GetBoolean(this NpgsqlDataReader rdr, string name) => rdr.GetBoolean(rdr.GetOrdinal(name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Get a 64-bit integer by its name
 | 
					        /// Get a 64-bit integer by its name
 | 
				
			||||||
@ -33,5 +33,19 @@ namespace JobsJobsJobs.Server.Data
 | 
				
			|||||||
        /// <returns>The specified field as milliseconds</returns>
 | 
					        /// <returns>The specified field as milliseconds</returns>
 | 
				
			||||||
        public static Milliseconds GetMilliseconds(this NpgsqlDataReader rdr, string name) =>
 | 
					        public static Milliseconds GetMilliseconds(this NpgsqlDataReader rdr, string name) =>
 | 
				
			||||||
            new Milliseconds(rdr.GetInt64(name));
 | 
					            new Milliseconds(rdr.GetInt64(name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Get a string by its name
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="name">The name of the field to be retrieved as a string</param>
 | 
				
			||||||
 | 
					        /// <returns>The specified field as a string</returns>
 | 
				
			||||||
 | 
					        public static string GetString(this NpgsqlDataReader rdr, string name) => rdr.GetString(rdr.GetOrdinal(name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Determine if a column is null
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="name">The name of the column to check</param>
 | 
				
			||||||
 | 
					        /// <returns>True if the column is null, false if not</returns>
 | 
				
			||||||
 | 
					        public static bool IsDBNull(this NpgsqlDataReader rdr, string name) => rdr.IsDBNull(rdr.GetOrdinal(name));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										52
									
								
								src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					using JobsJobsJobs.Shared;
 | 
				
			||||||
 | 
					using Npgsql;
 | 
				
			||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Collections.Generic;
 | 
				
			||||||
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace JobsJobsJobs.Server.Data
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Extensions to the NpgsqlConnection type to support manipulation of profiles
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    public static class ProfileExtensions
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Populate a profile object from the given data reader
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="rdr">The data reader from which values should be obtained</param>
 | 
				
			||||||
 | 
					        /// <returns>The populated profile</returns>
 | 
				
			||||||
 | 
					        private static Profile ToProfile(NpgsqlDataReader rdr)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var continentId = ContinentId.Parse(rdr.GetString("continent_id"));
 | 
				
			||||||
 | 
					            return new Profile(CitizenId.Parse(rdr.GetString("id")), rdr.GetBoolean("seeking_employment"),
 | 
				
			||||||
 | 
					                rdr.GetBoolean("is_public"), continentId, rdr.GetString("region"), rdr.GetBoolean("remote_work"),
 | 
				
			||||||
 | 
					                rdr.GetBoolean("full_time"), new MarkdownString(rdr.GetString("biography")),
 | 
				
			||||||
 | 
					                rdr.GetMilliseconds("last_updated_on"),
 | 
				
			||||||
 | 
					                rdr.IsDBNull("experience") ? null : new MarkdownString(rdr.GetString("experience")))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Continent = new Continent(continentId, rdr.GetString("continent_name"))
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Retrieve an employment profile by a citizen ID
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="citizen">The ID of the citizen whose profile should be retrieved</param>
 | 
				
			||||||
 | 
					        /// <returns>The profile, or null if it does not exist</returns>
 | 
				
			||||||
 | 
					        public static async Task<Profile?> FindProfileByCitizen(this NpgsqlConnection conn, CitizenId citizen)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            using var cmd = conn.CreateCommand();
 | 
				
			||||||
 | 
					            cmd.CommandText =
 | 
				
			||||||
 | 
					                @"SELECT p.*, c.name AS continent_name
 | 
				
			||||||
 | 
					                    FROM profile p
 | 
				
			||||||
 | 
					                        INNER JOIN continent c ON p.continent_id = c.id
 | 
				
			||||||
 | 
					                    WHERE citizen_id = @id";
 | 
				
			||||||
 | 
					            cmd.Parameters.Add(new NpgsqlParameter("@id", citizen.Id.ToString()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
 | 
				
			||||||
 | 
					            return await rdr.ReadAsync().ConfigureAwait(false) ? ToProfile(rdr) : null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,6 +6,7 @@
 | 
				
			|||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.0" />
 | 
					    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.0" />
 | 
				
			||||||
    <PackageReference Include="Npgsql" Version="5.0.0" />
 | 
					    <PackageReference Include="Npgsql" Version="5.0.0" />
 | 
				
			||||||
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
 | 
					    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
using JobsJobsJobs.Server.Data;
 | 
					                                                                                                                                using JobsJobsJobs.Server.Data;
 | 
				
			||||||
 | 
					using Microsoft.AspNetCore.Authentication.JwtBearer;
 | 
				
			||||||
using Microsoft.AspNetCore.Builder;
 | 
					using Microsoft.AspNetCore.Builder;
 | 
				
			||||||
using Microsoft.AspNetCore.Hosting;
 | 
					using Microsoft.AspNetCore.Hosting;
 | 
				
			||||||
using Microsoft.AspNetCore.HttpsPolicy;
 | 
					using Microsoft.AspNetCore.HttpsPolicy;
 | 
				
			||||||
@ -6,8 +7,10 @@ using Microsoft.AspNetCore.ResponseCompression;
 | 
				
			|||||||
using Microsoft.Extensions.Configuration;
 | 
					using Microsoft.Extensions.Configuration;
 | 
				
			||||||
using Microsoft.Extensions.DependencyInjection;
 | 
					using Microsoft.Extensions.DependencyInjection;
 | 
				
			||||||
using Microsoft.Extensions.Hosting;
 | 
					using Microsoft.Extensions.Hosting;
 | 
				
			||||||
 | 
					using Microsoft.IdentityModel.Tokens;
 | 
				
			||||||
using Npgsql;
 | 
					using Npgsql;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace JobsJobsJobs.Server
 | 
					namespace JobsJobsJobs.Server
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -25,8 +28,27 @@ namespace JobsJobsJobs.Server
 | 
				
			|||||||
        public void ConfigureServices(IServiceCollection services)
 | 
					        public void ConfigureServices(IServiceCollection services)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            services.AddScoped(_ => new NpgsqlConnection(Configuration.GetConnectionString("JobsDb")));
 | 
					            services.AddScoped(_ => new NpgsqlConnection(Configuration.GetConnectionString("JobsDb")));
 | 
				
			||||||
 | 
					            services.AddLogging();
 | 
				
			||||||
            services.AddControllersWithViews();
 | 
					            services.AddControllersWithViews();
 | 
				
			||||||
            services.AddRazorPages();
 | 
					            services.AddRazorPages();
 | 
				
			||||||
 | 
					            services.AddAuthentication(options =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
 | 
				
			||||||
 | 
					                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
 | 
				
			||||||
 | 
					                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
 | 
				
			||||||
 | 
					            }).AddJwtBearer(options =>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                options.RequireHttpsMetadata = false;
 | 
				
			||||||
 | 
					                options.TokenValidationParameters = new TokenValidationParameters
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ValidateIssuer = true,
 | 
				
			||||||
 | 
					                    ValidateAudience = true,
 | 
				
			||||||
 | 
					                    ValidAudience = "https://jobsjobs.jobs",
 | 
				
			||||||
 | 
					                    ValidIssuer = "https://jobsjobs.jobs",
 | 
				
			||||||
 | 
					                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
 | 
				
			||||||
 | 
					                        Configuration.GetSection("Auth")["ServerSecret"]))
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
 | 
					        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
 | 
				
			||||||
@ -52,6 +74,8 @@ namespace JobsJobsJobs.Server
 | 
				
			|||||||
            //       Alternately, maybe we can even configure the default services and middleware so that it will work
 | 
					            //       Alternately, maybe we can even configure the default services and middleware so that it will work
 | 
				
			||||||
            //       to give us the currently-logged-on user.
 | 
					            //       to give us the currently-logged-on user.
 | 
				
			||||||
            app.UseRouting();
 | 
					            app.UseRouting();
 | 
				
			||||||
 | 
					            app.UseAuthentication();
 | 
				
			||||||
 | 
					            app.UseAuthorization();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            app.UseEndpoints(endpoints =>
 | 
					            app.UseEndpoints(endpoints =>
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
				
			|||||||
@ -12,5 +12,13 @@ namespace JobsJobsJobs.Shared
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        /// <returns>A new continent ID</returns>
 | 
					        /// <returns>A new continent ID</returns>
 | 
				
			||||||
        public static async Task<ContinentId> Create() => new ContinentId(await ShortId.Create());
 | 
					        public static async Task<ContinentId> Create() => new ContinentId(await ShortId.Create());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Attempt to create a continent ID from a string
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        /// <param name="id">The prospective ID</param>
 | 
				
			||||||
 | 
					        /// <returns>The continent ID</returns>
 | 
				
			||||||
 | 
					        /// <exception cref="System.FormatException">If the string is not a valid continent ID</exception>
 | 
				
			||||||
 | 
					        public static ContinentId Parse(string id) => new ContinentId(ShortId.Parse(id));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user