Help wanted (#23)
Create a "help wanted" area of the site (#14)
This commit was merged in pull request #23.
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "5.0.1",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NodaTime;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// API controller for citizen information
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class CitizenController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization configuration section
|
||||
/// </summary>
|
||||
private readonly IConfigurationSection _config;
|
||||
|
||||
/// <summary>
|
||||
/// NodaTime clock
|
||||
/// </summary>
|
||||
private readonly IClock _clock;
|
||||
|
||||
/// <summary>
|
||||
/// The data context to use for this request
|
||||
/// </summary>
|
||||
private readonly JobsDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="config">The authorization configuration section</param>
|
||||
/// <param name="clock">The NodaTime clock instance</param>
|
||||
/// <param name="db">The data context to use for this request</param>
|
||||
public CitizenController(IConfiguration config, IClock clock, JobsDbContext db)
|
||||
{
|
||||
_config = config.GetSection("Auth");
|
||||
_clock = clock;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current citizen ID
|
||||
/// </summary>
|
||||
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||
|
||||
[HttpGet("log-on/{authCode}")]
|
||||
public async Task<IActionResult> LogOn([FromRoute] string authCode)
|
||||
{
|
||||
// Step 1 - Verify with Mastodon
|
||||
var accountResult = await Auth.VerifyWithMastodon(authCode, _config);
|
||||
|
||||
if (accountResult.IsError) return BadRequest(accountResult.Error);
|
||||
|
||||
// Step 2 - Find / establish Jobs, Jobs, Jobs account
|
||||
var account = accountResult.Ok;
|
||||
var now = _clock.GetCurrentInstant();
|
||||
|
||||
var citizen = await _db.FindCitizenByNAUser(account.Username);
|
||||
if (citizen == null)
|
||||
{
|
||||
citizen = new Citizen(await CitizenId.Create(), account.Username,
|
||||
string.IsNullOrWhiteSpace(account.DisplayName) ? null : account.DisplayName, null, account.Url,
|
||||
now, now);
|
||||
await _db.AddCitizen(citizen);
|
||||
}
|
||||
else
|
||||
{
|
||||
citizen = citizen with
|
||||
{
|
||||
DisplayName = string.IsNullOrWhiteSpace(account.DisplayName) ? null : account.DisplayName,
|
||||
LastSeenOn = now
|
||||
};
|
||||
_db.UpdateCitizen(citizen);
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Step 3 - Generate JWT
|
||||
var jwt = Auth.CreateJwt(citizen, _config);
|
||||
|
||||
return new JsonResult(new LogOnSuccess(jwt, citizen.Id.ToString(), citizen.CitizenName));
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("get/{id}")]
|
||||
public async Task<IActionResult> GetCitizenById([FromRoute] string id)
|
||||
{
|
||||
var citizen = await _db.FindCitizenById(CitizenId.Parse(id));
|
||||
return citizen == null ? NotFound() : Ok(citizen);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("")]
|
||||
public async Task<IActionResult> Remove()
|
||||
{
|
||||
await _db.DeleteCitizen(CurrentCitizenId);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// API endpoint for continent information
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class ContinentController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The data context to use for this request
|
||||
/// </summary>
|
||||
private readonly JobsDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="db">The data context to use for this request</param>
|
||||
public ContinentController(JobsDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> All() =>
|
||||
Ok(await _db.AllContinents());
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// API controller for employment profile information
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class ProfileController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The data context
|
||||
/// </summary>
|
||||
private readonly JobsDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// The NodaTime clock instance
|
||||
/// </summary>
|
||||
private readonly IClock _clock;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="db">The data context to use for this request</param>
|
||||
/// <param name="clock">The clock instance to use for this request</param>
|
||||
public ProfileController(JobsDbContext db, IClock clock)
|
||||
{
|
||||
_db = db;
|
||||
_clock = clock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current citizen ID
|
||||
/// </summary>
|
||||
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||
|
||||
// This returns 204 to indicate that there is no profile data for the current citizen (if, of course, that is
|
||||
// the case). The version where an ID is specified returns 404, which is an error condition, as it should not
|
||||
// occur unless someone is messing with a URL.
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Get()
|
||||
{
|
||||
var profile = await _db.FindProfileByCitizen(CurrentCitizenId);
|
||||
return profile == null ? NoContent() : Ok(profile);
|
||||
}
|
||||
|
||||
[HttpPost("save")]
|
||||
public async Task<IActionResult> Save(ProfileForm form)
|
||||
{
|
||||
// Profile
|
||||
var existing = await _db.FindProfileByCitizen(CurrentCitizenId);
|
||||
var profile = existing == null
|
||||
? new Profile(CurrentCitizenId, form.IsSeekingEmployment, form.IsPublic,
|
||||
ContinentId.Parse(form.ContinentId), form.Region, form.RemoteWork, form.FullTime,
|
||||
new MarkdownString(form.Biography), _clock.GetCurrentInstant(),
|
||||
string.IsNullOrEmpty(form.Experience) ? null : new MarkdownString(form.Experience))
|
||||
: existing with
|
||||
{
|
||||
SeekingEmployment = form.IsSeekingEmployment,
|
||||
IsPublic = form.IsPublic,
|
||||
ContinentId = ContinentId.Parse(form.ContinentId),
|
||||
Region = form.Region,
|
||||
RemoteWork = form.RemoteWork,
|
||||
FullTime = form.FullTime,
|
||||
Biography = new MarkdownString(form.Biography),
|
||||
LastUpdatedOn = _clock.GetCurrentInstant(),
|
||||
Experience = string.IsNullOrEmpty(form.Experience) ? null : new MarkdownString(form.Experience)
|
||||
};
|
||||
await _db.SaveProfile(profile);
|
||||
|
||||
// Skills
|
||||
var skills = new List<Skill>();
|
||||
foreach (var skill in form.Skills) {
|
||||
skills.Add(new Skill(skill.Id.StartsWith("new") ? await SkillId.Create() : SkillId.Parse(skill.Id),
|
||||
CurrentCitizenId, skill.Description, string.IsNullOrEmpty(skill.Notes) ? null : skill.Notes));
|
||||
}
|
||||
|
||||
foreach (var skill in skills) await _db.SaveSkill(skill);
|
||||
await _db.DeleteMissingSkills(CurrentCitizenId, skills.Select(s => s.Id));
|
||||
|
||||
// Real Name
|
||||
_db.Update((await _db.FindCitizenById(CurrentCitizenId))!
|
||||
with { RealName = string.IsNullOrWhiteSpace(form.RealName) ? null : form.RealName });
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("count")]
|
||||
public async Task<IActionResult> GetProfileCount() =>
|
||||
Ok(new Count(await _db.CountProfiles()));
|
||||
|
||||
[HttpGet("skill-count")]
|
||||
public async Task<IActionResult> GetSkillCount() =>
|
||||
Ok(new Count(await _db.CountSkillsByCitizen(CurrentCitizenId)));
|
||||
|
||||
[HttpGet("get/{id}")]
|
||||
public async Task<IActionResult> GetProfileForCitizen([FromRoute] string id)
|
||||
{
|
||||
var profile = await _db.FindProfileByCitizen(CitizenId.Parse(id));
|
||||
return profile == null ? NotFound() : Ok(profile);
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> Search([FromQuery] ProfileSearch search) =>
|
||||
Ok(await _db.SearchProfiles(search));
|
||||
|
||||
[HttpGet("public-search")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> SearchPublic([FromQuery] PublicSearch search) =>
|
||||
Ok(await _db.SearchPublicProfiles(search));
|
||||
|
||||
[HttpPatch("employment-found")]
|
||||
public async Task<IActionResult> EmploymentFound()
|
||||
{
|
||||
var profile = await _db.FindProfileByCitizen(CurrentCitizenId);
|
||||
if (profile == null) return NotFound();
|
||||
|
||||
var updated = profile with { SeekingEmployment = false };
|
||||
_db.Update(updated);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
public async Task<IActionResult> Remove()
|
||||
{
|
||||
await _db.DeleteProfileByCitizen(CurrentCitizenId);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// API controller for success stories
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class SuccessController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// The data context
|
||||
/// </summary>
|
||||
private readonly JobsDbContext _db;
|
||||
|
||||
/// <summary>
|
||||
/// The NodaTime clock instance
|
||||
/// </summary>
|
||||
private readonly IClock _clock;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="db">The data context to use for this request</param>
|
||||
/// <param name="clock">The clock instance to use for this request</param>
|
||||
public SuccessController(JobsDbContext db, IClock clock)
|
||||
{
|
||||
_db = db;
|
||||
_clock = clock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current citizen ID
|
||||
/// </summary>
|
||||
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Retrieve(string id) =>
|
||||
Ok(await _db.FindSuccessById(SuccessId.Parse(id)));
|
||||
|
||||
[HttpPost("save")]
|
||||
public async Task<IActionResult> Save([FromBody] StoryForm form)
|
||||
{
|
||||
if (form.Id == "new")
|
||||
{
|
||||
var story = new Success(await SuccessId.Create(), CurrentCitizenId, _clock.GetCurrentInstant(),
|
||||
form.FromHere, string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story));
|
||||
await _db.AddAsync(story);
|
||||
}
|
||||
else
|
||||
{
|
||||
var story = await _db.FindSuccessById(SuccessId.Parse(form.Id));
|
||||
if (story == null) return NotFound();
|
||||
var updated = story with
|
||||
{
|
||||
FromHere = form.FromHere,
|
||||
Story = string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story)
|
||||
};
|
||||
_db.Update(updated);
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> List() =>
|
||||
Ok(await _db.AllStories());
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
using JobsJobsJobs.Server.Models;
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication / authorization utility methods
|
||||
/// </summary>
|
||||
public static class Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify the authorization code with Mastodon and get the user's profile
|
||||
/// </summary>
|
||||
/// <param name="authCode">The code from the authorization flow</param>
|
||||
/// <param name="config">The authorization configuration section</param>
|
||||
/// <returns>The Mastodon account (or an error if one is encountered)</returns>
|
||||
public static async Task<Result<MastodonAccount>> VerifyWithMastodon(string authCode,
|
||||
IConfigurationSection config)
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
|
||||
// Use authorization code to get an access token from NAS
|
||||
using var codeResult = await http.PostAsJsonAsync("https://noagendasocial.com/oauth/token", new
|
||||
{
|
||||
client_id = config["ClientId"],
|
||||
client_secret = config["Secret"],
|
||||
redirect_uri = $"{config["ReturnHost"]}/citizen/authorized",
|
||||
grant_type = "authorization_code",
|
||||
code = authCode,
|
||||
scope = "read"
|
||||
});
|
||||
if (!codeResult.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"ERR: {await codeResult.Content.ReadAsStringAsync()}");
|
||||
return Result<MastodonAccount>.AsError(
|
||||
$"Could not get token ({codeResult.StatusCode:D}: {codeResult.ReasonPhrase})");
|
||||
}
|
||||
|
||||
using var tokenResponse = JsonSerializer.Deserialize<JsonDocument>(
|
||||
new ReadOnlySpan<byte>(await codeResult.Content.ReadAsByteArrayAsync()));
|
||||
if (tokenResponse == null)
|
||||
{
|
||||
return Result<MastodonAccount>.AsError("Could not parse authorization code result");
|
||||
}
|
||||
|
||||
var accessToken = tokenResponse.RootElement.GetProperty("access_token").GetString();
|
||||
|
||||
// Use access token to get profile from NAS
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, $"{config["ApiUrl"]}accounts/verify_credentials");
|
||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var profileResult = await http.SendAsync(req);
|
||||
if (!profileResult.IsSuccessStatusCode)
|
||||
{
|
||||
return Result<MastodonAccount>.AsError(
|
||||
$"Could not get profile ({profileResult.StatusCode:D}: {profileResult.ReasonPhrase})");
|
||||
}
|
||||
|
||||
var profileResponse = JsonSerializer.Deserialize<MastodonAccount>(
|
||||
new ReadOnlySpan<byte>(await profileResult.Content.ReadAsByteArrayAsync()));
|
||||
if (profileResponse == null)
|
||||
{
|
||||
return Result<MastodonAccount>.AsError("Could not parse profile result");
|
||||
}
|
||||
|
||||
if (profileResponse.Username != profileResponse.AccountName)
|
||||
{
|
||||
return Result<MastodonAccount>.AsError(
|
||||
$"Profiles must be from noagendasocial.com; yours is {profileResponse.AccountName}");
|
||||
}
|
||||
|
||||
return Result<MastodonAccount>.AsOk(profileResponse);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a JSON Web Token for this citizen to use for further requests to this API
|
||||
/// </summary>
|
||||
/// <param name="citizen">The citizen for which the token should be generated</param>
|
||||
/// <param name="config">The authorization configuration section</param>
|
||||
/// <returns>The JWT</returns>
|
||||
public static string CreateJwt(Citizen citizen, IConfigurationSection config)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, citizen.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, citizen.CitizenName),
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddHours(2),
|
||||
Issuer = "https://noagendacareers.com",
|
||||
Audience = "https://noagendacareers.com",
|
||||
SigningCredentials = new SigningCredentials(
|
||||
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["ServerSecret"])),
|
||||
SecurityAlgorithms.HmacSha256Signature)
|
||||
});
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to JobsDbContext supporting the manipulation of citizens
|
||||
/// </summary>
|
||||
public static class CitizenExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a citizen by their Jobs, Jobs, Jobs ID
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen to retrieve</param>
|
||||
/// <returns>The citizen, or null if not found</returns>
|
||||
public static async Task<Citizen?> FindCitizenById(this JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Citizens.AsNoTracking()
|
||||
.SingleOrDefaultAsync(c => c.Id == citizenId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a citizen by their No Agenda Social user name
|
||||
/// </summary>
|
||||
/// <param name="naUser">The NAS user name</param>
|
||||
/// <returns>The citizen, or null if not found</returns>
|
||||
public static async Task<Citizen?> FindCitizenByNAUser(this JobsDbContext db, string naUser) =>
|
||||
await db.Citizens.AsNoTracking()
|
||||
.SingleOrDefaultAsync(c => c.NaUser == naUser)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Add a citizen
|
||||
/// </summary>
|
||||
/// <param name="citizen">The citizen to be added</param>
|
||||
public static async Task AddCitizen(this JobsDbContext db, Citizen citizen) =>
|
||||
await db.Citizens.AddAsync(citizen).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Update a citizen after they have logged on (update last seen, sync display name)
|
||||
/// </summary>
|
||||
/// <param name="citizen">The updated citizen</param>
|
||||
public static void UpdateCitizen(this JobsDbContext db, Citizen citizen) =>
|
||||
db.Entry(citizen).State = EntityState.Modified;
|
||||
|
||||
/// <summary>
|
||||
/// Delete a citizen
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen to be deleted</param>
|
||||
/// <returns></returns>
|
||||
public static async Task DeleteCitizen(this JobsDbContext db, CitizenId citizenId)
|
||||
{
|
||||
var id = citizenId.ToString();
|
||||
await db.DeleteProfileByCitizen(citizenId).ConfigureAwait(false);
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.success WHERE citizen_id = {id}")
|
||||
.ConfigureAwait(false);
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.citizen WHERE id = {id}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Data extensions for manipulation of continent objects
|
||||
/// </summary>
|
||||
public static class ContinentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve all continents
|
||||
/// </summary>
|
||||
/// <returns>All continents</returns>
|
||||
public static async Task<IEnumerable<Continent>> AllContinents(this JobsDbContext db) =>
|
||||
await db.Continents.AsNoTracking().OrderBy(c => c.Name).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a continent by its ID
|
||||
/// </summary>
|
||||
/// <param name="continentId">The ID of the continent to retrieve</param>
|
||||
/// <returns>The continent matching the ID</returns>
|
||||
public static async Task<Continent> FindContinentById(this JobsDbContext db, ContinentId continentId) =>
|
||||
await db.Continents.AsNoTracking()
|
||||
.SingleAsync(c => c.Id == continentId)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Converters used to translate between database and domain types
|
||||
/// </summary>
|
||||
public static class Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Citizen ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<CitizenId, string> CitizenIdConverter =
|
||||
new(v => v.ToString(), v => CitizenId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Continent ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<ContinentId, string> ContinentIdConverter =
|
||||
new(v => v.ToString(), v => ContinentId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Markdown converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<MarkdownString, string> MarkdownStringConverter =
|
||||
new(v => v.Text, v => new MarkdownString(v));
|
||||
|
||||
/// <summary>
|
||||
/// Markdown converter for possibly-null values
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<MarkdownString?, string?> OptionalMarkdownStringConverter =
|
||||
new(v => v == null ? null : v.Text, v => v == null ? null : new MarkdownString(v));
|
||||
|
||||
/// <summary>
|
||||
/// Skill ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<SkillId, string> SkillIdConverter =
|
||||
new(v => v.ToString(), v => SkillId.Parse(v));
|
||||
|
||||
/// <summary>
|
||||
/// Success ID converter
|
||||
/// </summary>
|
||||
public static readonly ValueConverter<SuccessId, string> SuccessIdConverter =
|
||||
new(v => v.ToString(), v => SuccessId.Parse(v));
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Data context for Jobs, Jobs, Jobs
|
||||
/// </summary>
|
||||
public class JobsDbContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Citizens (users known to us)
|
||||
/// </summary>
|
||||
public DbSet<Citizen> Citizens { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Continents (large land masses - 7 of them!)
|
||||
/// </summary>
|
||||
public DbSet<Continent> Continents { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Employment profiles
|
||||
/// </summary>
|
||||
public DbSet<Profile> Profiles { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Skills held by citizens of Gitmo Nation
|
||||
/// </summary>
|
||||
public DbSet<Skill> Skills { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Success stories from the site
|
||||
/// </summary>
|
||||
public DbSet<Success> Successes { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="options">The options to use to configure this instance</param>
|
||||
public JobsDbContext(DbContextOptions<JobsDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Citizen>(m =>
|
||||
{
|
||||
m.ToTable("citizen", "jjj").HasKey(e => e.Id);
|
||||
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.CitizenIdConverter);
|
||||
m.Property(e => e.NaUser).HasColumnName("na_user").IsRequired().HasMaxLength(50);
|
||||
m.Property(e => e.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
||||
m.Property(e => e.ProfileUrl).HasColumnName("profile_url").IsRequired().HasMaxLength(1_024);
|
||||
m.Property(e => e.JoinedOn).HasColumnName("joined_on").IsRequired();
|
||||
m.Property(e => e.LastSeenOn).HasColumnName("last_seen_on").IsRequired();
|
||||
m.Property(e => e.RealName).HasColumnName("real_name").HasMaxLength(255);
|
||||
m.HasIndex(e => e.NaUser).IsUnique();
|
||||
m.Ignore(e => e.CitizenName);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Continent>(m =>
|
||||
{
|
||||
m.ToTable("continent", "jjj").HasKey(e => e.Id);
|
||||
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.ContinentIdConverter);
|
||||
m.Property(e => e.Name).HasColumnName("name").IsRequired().HasMaxLength(255);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Profile>(m =>
|
||||
{
|
||||
m.ToTable("profile", "jjj").HasKey(e => e.Id);
|
||||
m.Property(e => e.Id).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.CitizenIdConverter);
|
||||
m.Property(e => e.SeekingEmployment).HasColumnName("seeking_employment").IsRequired();
|
||||
m.Property(e => e.IsPublic).HasColumnName("is_public").IsRequired();
|
||||
m.Property(e => e.ContinentId).HasColumnName("continent_id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.ContinentIdConverter);
|
||||
m.Property(e => e.Region).HasColumnName("region").IsRequired().HasMaxLength(255);
|
||||
m.Property(e => e.RemoteWork).HasColumnName("remote_work").IsRequired();
|
||||
m.Property(e => e.FullTime).HasColumnName("full_time").IsRequired();
|
||||
m.Property(e => e.Biography).HasColumnName("biography").IsRequired()
|
||||
.HasConversion(Converters.MarkdownStringConverter);
|
||||
m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired();
|
||||
m.Property(e => e.Experience).HasColumnName("experience")
|
||||
.HasConversion(Converters.OptionalMarkdownStringConverter);
|
||||
m.HasOne(e => e.Continent)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ContinentId);
|
||||
m.HasMany(e => e.Skills)
|
||||
.WithOne()
|
||||
.HasForeignKey(e => e.CitizenId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Skill>(m =>
|
||||
{
|
||||
m.ToTable("skill", "jjj").HasKey(e => e.Id);
|
||||
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.SkillIdConverter);
|
||||
m.Property(e => e.CitizenId).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.CitizenIdConverter);
|
||||
m.Property(e => e.Description).HasColumnName("skill").IsRequired().HasMaxLength(100);
|
||||
m.Property(e => e.Notes).HasColumnName("notes").HasMaxLength(100);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Success>(m =>
|
||||
{
|
||||
m.ToTable("success", "jjj").HasKey(e => e.Id);
|
||||
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.SuccessIdConverter);
|
||||
m.Property(e => e.CitizenId).HasColumnName("citizen_id").IsRequired().HasMaxLength(12)
|
||||
.HasConversion(Converters.CitizenIdConverter);
|
||||
m.Property(e => e.RecordedOn).HasColumnName("recorded_on").IsRequired();
|
||||
m.Property(e => e.FromHere).HasColumnName("from_here").IsRequired();
|
||||
m.Property(e => e.Story).HasColumnName("story")
|
||||
.HasConversion(Converters.OptionalMarkdownStringConverter);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to JobsDbContext to support manipulation of profiles
|
||||
/// </summary>
|
||||
public static class ProfileExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve an employment profile by a citizen ID
|
||||
/// </summary>
|
||||
/// <param name="citizenId">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 JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Profiles.AsNoTracking()
|
||||
.Include(p => p.Continent)
|
||||
.Include(p => p.Skills)
|
||||
.SingleOrDefaultAsync(p => p.Id == citizenId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Save a profile
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to be saved</param>
|
||||
public static async Task SaveProfile(this JobsDbContext db, Profile profile)
|
||||
{
|
||||
if (await db.Profiles.CountAsync(p => p.Id == profile.Id).ConfigureAwait(false) == 0)
|
||||
{
|
||||
await db.AddAsync(profile).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
db.Entry(profile).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a skill
|
||||
/// </summary>
|
||||
/// <param name="skill">The skill to be saved</param>
|
||||
public static async Task SaveSkill(this JobsDbContext db, Skill skill)
|
||||
{
|
||||
if (await db.Skills.CountAsync(s => s.Id == skill.Id).ConfigureAwait(false) == 0)
|
||||
{
|
||||
await db.Skills.AddAsync(skill).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
db.Entry(skill).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete any skills that are not in the list of current skill IDs
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen to whom the skills belong</param>
|
||||
/// <param name="ids">The IDs of their current skills</param>
|
||||
public static async Task DeleteMissingSkills(this JobsDbContext db, CitizenId citizenId,
|
||||
IEnumerable<SkillId> ids)
|
||||
{
|
||||
if (!ids.Any()) return;
|
||||
|
||||
db.Skills.RemoveRange(await db.Skills.AsNoTracking()
|
||||
.Where(s => s.CitizenId == citizenId && !ids.Contains(s.Id)).ToListAsync()
|
||||
.ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a count of the citizens with profiles
|
||||
/// </summary>
|
||||
/// <returns>The number of citizens with profiles</returns>
|
||||
public static async Task<int> CountProfiles(this JobsDbContext db) =>
|
||||
await db.Profiles.CountAsync().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Count the skills for the given citizen
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen whose skills should be counted</param>
|
||||
/// <returns>The count of skills for the given citizen</returns>
|
||||
public static async Task<int> CountSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Skills.CountAsync(s => s.CitizenId == citizenId).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Search profiles by the given criteria
|
||||
/// </summary>
|
||||
/// <param name="search">The search parameters</param>
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db,
|
||||
ProfileSearch search)
|
||||
{
|
||||
var query = db.Profiles
|
||||
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c });
|
||||
|
||||
var useIds = false;
|
||||
var citizenIds = new List<CitizenId>();
|
||||
|
||||
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||
{
|
||||
query = query.Where(it => it.Profile.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||
{
|
||||
query = query.Where(it => it.Profile.RemoteWork == (search.RemoteWork == "yes"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.Skill))
|
||||
{
|
||||
useIds = true;
|
||||
citizenIds.AddRange(await db.Skills
|
||||
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||
.Select(s => s.CitizenId)
|
||||
.ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.BioExperience))
|
||||
{
|
||||
useIds = true;
|
||||
citizenIds.AddRange(await db.Profiles
|
||||
.FromSqlRaw("SELECT citizen_id FROM profile WHERE biography ILIKE {0} OR experience ILIKE {0}",
|
||||
$"%{search.BioExperience}%")
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (useIds)
|
||||
{
|
||||
query = query.Where(it => citizenIds.Contains(it.Citizen.Id));
|
||||
}
|
||||
|
||||
return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.CitizenName,
|
||||
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search public profiles by the given criteria
|
||||
/// </summary>
|
||||
/// <param name="search">The search parameters</param>
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<PublicSearchResult>> SearchPublicProfiles(this JobsDbContext db,
|
||||
PublicSearch search)
|
||||
{
|
||||
var query = db.Profiles
|
||||
.Include(it => it.Continent)
|
||||
.Include(it => it.Skills)
|
||||
.Where(it => it.IsPublic);
|
||||
|
||||
var useIds = false;
|
||||
var citizenIds = new List<CitizenId>();
|
||||
|
||||
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||
{
|
||||
query = query.Where(it => it.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.Region))
|
||||
{
|
||||
query = query.Where(it => it.Region.ToLower().Contains(search.Region.ToLower()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||
{
|
||||
query = query.Where(it => it.RemoteWork == (search.RemoteWork == "yes"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(search.Skill))
|
||||
{
|
||||
useIds = true;
|
||||
citizenIds.AddRange(await db.Skills
|
||||
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||
.Select(s => s.CitizenId)
|
||||
.ToListAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
if (useIds)
|
||||
{
|
||||
query = query.Where(it => citizenIds.Contains(it.Id));
|
||||
}
|
||||
|
||||
return await query.Select(x => new PublicSearchResult(x.Continent!.Name, x.Region, x.RemoteWork,
|
||||
x.Skills.Select(sk => $"{sk.Description} ({sk.Notes})")))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete skills and profile for the given citizen
|
||||
/// </summary>
|
||||
/// <param name="citizenId">The ID of the citizen whose profile should be deleted</param>
|
||||
public static async Task DeleteProfileByCitizen(this JobsDbContext db, CitizenId citizenId)
|
||||
{
|
||||
var id = citizenId.ToString();
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.skill WHERE citizen_id = {id}")
|
||||
.ConfigureAwait(false);
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.profile WHERE citizen_id = {id}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions to JobsDbContext to support manipulation of success stories
|
||||
/// </summary>
|
||||
public static class SuccessExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a success story by its ID
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the story to retrieve</param>
|
||||
/// <returns>The success story, if found</returns>
|
||||
public static async Task<Success?> FindSuccessById(this JobsDbContext db, SuccessId id) =>
|
||||
await db.Successes.AsNoTracking().SingleOrDefaultAsync(s => s.Id == id).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of success stories, with the information needed for the list page
|
||||
/// </summary>
|
||||
/// <returns>A list of success stories, citizen names, and dates</returns>
|
||||
public static async Task<IEnumerable<StoryEntry>> AllStories(this JobsDbContext db) =>
|
||||
await db.Successes
|
||||
.Join(db.Citizens, s => s.CitizenId, c => c.Id, (s, c) => new { Success = s, Citizen = c })
|
||||
.OrderByDescending(it => it.Success.RecordedOn)
|
||||
.Select(it => new StoryEntry(it.Success.Id, it.Citizen.Id, it.Citizen.CitizenName,
|
||||
it.Success.RecordedOn, it.Success.FromHere, it.Success.Story != null))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<UserSecretsId>553960ef-0c79-47d4-98d8-9ca1708e558f</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.1" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.1.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="5.0.1" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="5.0.1.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Client\JobsJobsJobs.Client.csproj" />
|
||||
<ProjectReference Include="..\Shared\JobsJobsJobs.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<RazorPage_SelectedScaffolderID>RazorPageScaffolder</RazorPage_SelectedScaffolderID>
|
||||
<RazorPage_SelectedScaffolderCategoryPath>root/Common/RazorPage</RazorPage_SelectedScaffolderCategoryPath>
|
||||
<ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile>
|
||||
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
||||
<NameOfLastUsedPublishProfile>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JobsJobsJobs.Server.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// The variables we need from the account information we get from No Agenda Social
|
||||
/// </summary>
|
||||
public class MastodonAccount
|
||||
{
|
||||
/// <summary>
|
||||
/// The user name (what we store as naUser)
|
||||
/// </summary>
|
||||
[JsonPropertyName("username")]
|
||||
public string Username { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The account name; will be the same as username for local (non-federated) accounts
|
||||
/// </summary>
|
||||
[JsonPropertyName("acct")]
|
||||
public string AccountName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The user's display name as it currently shows on No Agenda Social
|
||||
/// </summary>
|
||||
[JsonPropertyName("display_name")]
|
||||
public string DisplayName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The user's profile URL
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@page
|
||||
@model JobsJobsJobs.Server.Pages.ErrorModel
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Error</title>
|
||||
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="~/css/app.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="content px-4">
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,32 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Pages
|
||||
{
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string RequestId { get; set; } = default!;
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
@page
|
||||
@model JobsJobsJobs.Server.Pages._HostModel
|
||||
@{
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Jobs, Jobs, Jobs</title>
|
||||
<base href="/" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<link href="JobsJobsJobs.Client.styles.css" rel="stylesheet" />
|
||||
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<component type="typeof(JobsJobsJobs.Client.App)" render-mode="WebAssemblyPrerendered" />
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script>
|
||||
var Audio = {
|
||||
play(audio) {
|
||||
document.getElementById(audio).play()
|
||||
}
|
||||
}
|
||||
function setPageTitle(theTitle) {
|
||||
document.title = theTitle
|
||||
}
|
||||
function getTimeZone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JobsJobsJobs.Server.Pages
|
||||
{
|
||||
public class _HostModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<DeleteExistingFiles>True</DeleteExistingFiles>
|
||||
<ExcludeApp_Data>False</ExcludeApp_Data>
|
||||
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<PublishProvider>FileSystem</PublishProvider>
|
||||
<PublishUrl>bin\Release\net5.0\publish\</PublishUrl>
|
||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<PublishSingleFile>True</PublishSingleFile>
|
||||
<PublishTrimmed>True</PublishTrimmed>
|
||||
<ProjectGuid>35aeecbf-489d-41c5-9ba3-6e43ad7a8196</ProjectGuid>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<_PublishTargetUrl>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\bin\Release\net5.0\publish\</_PublishTargetUrl>
|
||||
<History>True|2021-06-15T02:08:33.4261507Z;True|2021-06-14T21:58:04.2622487-04:00;True|2021-03-16T19:34:57.2747439-04:00;</History>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:49363",
|
||||
"sslPort": 44308
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"JobsJobsJobs.Server": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:3005;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using Blazored.Toast;
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using Npgsql;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// TODO: configure JSON serialization for NodaTime
|
||||
services.AddDbContext<JobsDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("JobsDb"), o => o.UseNodaTime());
|
||||
#if DEBUG
|
||||
options.LogTo(System.Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
|
||||
#endif
|
||||
});
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddLogging();
|
||||
services.AddControllersWithViews();
|
||||
services.AddRazorPages()
|
||||
.AddJsonOptions(options =>
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
|
||||
services.AddBlazoredToast();
|
||||
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://noagendacareers.com",
|
||||
ValidIssuer = "https://noagendacareers.com",
|
||||
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.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
NpgsqlConnection.GlobalTypeMapper.UseNodaTime();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseWebAssemblyDebugging();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
static Task send404(HttpContext context)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapFallback("api/{**slug}", send404);
|
||||
endpoints.MapFallbackToPage("{**slug}", "/_Host");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Auth": {
|
||||
"ApiUrl": "https://noagendasocial.com/api/v1/",
|
||||
"ReturnHost": "https://noagendacareers.com"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user