Compare commits
2 Commits
1b46010074
...
68aa1e72a0
Author | SHA1 | Date | |
---|---|---|---|
68aa1e72a0 | |||
15657df008 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
# Docker Container Persistant files
|
||||
Postgres/pg-db_data/*
|
||||
Postgres/pg-admin_data/*
|
3
Postgres/.env
Normal file
3
Postgres/.env
Normal file
@ -0,0 +1,3 @@
|
||||
POSTGRES_PASSWORD=nqA3UV3CliLLHpLL
|
||||
PGADMIN_DEFAULT_EMAIL=admin@admin.com
|
||||
PGADMIN_DEFAULT_PASSWORD=3254
|
33
Postgres/docker-compose.yml
Normal file
33
Postgres/docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
database:
|
||||
container_name: activeallocator-pg-db
|
||||
image: 'postgres:15'
|
||||
ports:
|
||||
- 15432:5432
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- postgres-network
|
||||
volumes:
|
||||
- ./pg-db_data/:/var/lib/postgresql/data/
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
|
||||
management_interface:
|
||||
container_name: activeallocator-pg-admin
|
||||
image: 'dpage/pgadmin4:7.1'
|
||||
ports:
|
||||
- 15433:80
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- database
|
||||
networks:
|
||||
- postgres-network
|
||||
volumes:
|
||||
- ./pg-admin_data/:/var/lib/pgadmin/
|
||||
|
||||
networks:
|
||||
postgres-network:
|
||||
driver: bridge
|
3
Postgres/init.sql
Normal file
3
Postgres/init.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Create the DB if it doesn't already exist
|
||||
SELECT 'CREATE DATABASE AADB'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'AADB')\gexec
|
68
README.md
68
README.md
@ -1,3 +1,67 @@
|
||||
# ActiveAllocator
|
||||
# Active Allocator - React & .NET Core API
|
||||
|
||||
An API and Web App for managing budgets with allocations (buckets of funds based on history) and interfacing with Banks, and FireFlyIII.
|
||||
An API and Web App for managing budgets with allocations (buckets of funds based on history) and interfacing with Banks, and FireFlyIII.
|
||||
|
||||
I started with [this](https://travis.media/how-to-create-react-app-net-api-vscode/) tutorial.
|
||||
[Here](https://jasonwatmore.com/post/2022/03/15/net-6-crud-api-example-and-tutorial#user-service-cs) is another one that is handy for good structure.
|
||||
|
||||
Reference for adding JWT authentication is [here](https://jasonwatmore.com/net-7-csharp-jwt-authentication-tutorial-without-aspnet-core-identity#program-cs).
|
||||
|
||||
[Docker Repository Usage](https://www.ionos.com/digitalguide/server/know-how/setting-up-a-docker-repository/)
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
On Archlinux install the following to use dotnet: ```sudo pacman -Sy dotnet-sdk dotnet-runtime aspnet-runtime```.
|
||||
|
||||
For Archlinux install docker with ```sudo pacman -Sy docker docker-compose```.
|
||||
|
||||
Then run ```systemctl start docker.service``` and ```systemctl enable docker.service``` to start and enable on boot the docker engine.
|
||||
|
||||
Install React dependencies if needed - ```npm install``` in the ClientApp directory.
|
||||
|
||||
### Download 'Reverse Engineering' Tools
|
||||
|
||||
```
|
||||
dotnet tool install --global dotnet-ef
|
||||
dotnet tool install --global dotnet-aspnet-codegenerator
|
||||
```
|
||||
|
||||
Confirm it is working with ```dotnet ef```.
|
||||
|
||||
## Scaffolding
|
||||
|
||||
1. Create Entity (or Model) for object.
|
||||
2. Add ```DbSet<object>``` entry in your DbContext.
|
||||
3. Migration
|
||||
4. Endpoints (if needed)
|
||||
|
||||
To add Migration for new entity:
|
||||
```
|
||||
dotnet ef migrations add InitialCreate
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
For generating API endpoints for an entity called ```Note```:
|
||||
```
|
||||
$HOME/.dotnet/tools/dotnet-aspnet-codegenerator controller -name NotesController -async -api -m Note -dc MyDBContext --relativeFolderPath Controllers
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
### Create Docker Image
|
||||
|
||||
Running ```sudo dotnet publish``` will create a docker image targetting linux x64 with the "DefaultContainer" profile and image name of "cavaliercraftsmen.site". Edit the .csproj file to change the version of the image.
|
||||
|
||||
To create a docker image explicitly run ```sudo dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:ContainerImageName=cavaliercraftsmen.site``` in the same folder as the dotnet solution. Because we added the ```Microsoft.NET.Build.Containers``` package, we don't actually need to use the Dockerfile.
|
||||
|
||||
### Pushing Docker Image
|
||||
|
||||
You will have to be logged in. ```sudo docker login gitea.veritablevalor.com```
|
||||
|
||||
First tag ```sudo docker tag <image_id> gitea.veritablevalor.com/blizliam/cavaliercraftsmen.site:latest``` (check that the version is right).
|
||||
|
||||
Then push ```sudo docker push gitea.veritablevalor.com/blizliam/cavaliercraftsmen.site:latest```.
|
||||
|
||||
## Using API
|
||||
|
||||
API should be something like - https://localhost:7260/swagger
|
232
active-allocator/.gitignore
vendored
Normal file
232
active-allocator/.gitignore
vendored
Normal file
@ -0,0 +1,232 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
bin/
|
||||
Bin/
|
||||
obj/
|
||||
Obj/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
/wwwroot/dist/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Microsoft Azure ApplicationInsights config file
|
||||
ApplicationInsights.config
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
/node_modules
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
@ -0,0 +1,5 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class AllowAnonymousAttribute : Attribute
|
||||
{ }
|
22
active-allocator/Authorization/AuthorizeAttribute.cs
Normal file
22
active-allocator/Authorization/AuthorizeAttribute.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using active_allocator.Entities;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
// skip authorization if action is decorated with [AllowAnonymous] attribute
|
||||
var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
|
||||
if (allowAnonymous)
|
||||
return;
|
||||
|
||||
// authorization
|
||||
var user = (User?)context.HttpContext.Items["User"];
|
||||
if (user == null)
|
||||
context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
|
||||
}
|
||||
}
|
26
active-allocator/Authorization/JwtMiddleware.cs
Normal file
26
active-allocator/Authorization/JwtMiddleware.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using active_allocator.Services;
|
||||
|
||||
public class JwtMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public JwtMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
|
||||
{
|
||||
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
||||
var userId = jwtUtils.ValidateToken(token);
|
||||
if (userId != null)
|
||||
{
|
||||
// attach user to context on successful jwt validation
|
||||
context.Items["User"] = userService.GetById(userId.Value);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
72
active-allocator/Authorization/JwtUtils.cs
Normal file
72
active-allocator/Authorization/JwtUtils.cs
Normal file
@ -0,0 +1,72 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
public interface IJwtUtils
|
||||
{
|
||||
public string GenerateToken(User user);
|
||||
public int? ValidateToken(string token);
|
||||
}
|
||||
|
||||
public class JwtUtils : IJwtUtils
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public JwtUtils(IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_appSettings = appSettings.Value;
|
||||
}
|
||||
|
||||
public string GenerateToken(User user)
|
||||
{
|
||||
// generate token that is valid for 7 days
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
|
||||
Expires = DateTime.UtcNow.AddDays(7),
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
public int? ValidateToken(string token)
|
||||
{
|
||||
if (token == null)
|
||||
return null;
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
try
|
||||
{
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
|
||||
ClockSkew = TimeSpan.Zero
|
||||
}, out SecurityToken validatedToken);
|
||||
|
||||
var jwtToken = (JwtSecurityToken)validatedToken;
|
||||
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
|
||||
|
||||
// return user id from JWT token if validation successful
|
||||
return userId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// return null if validation fails
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
1
active-allocator/ClientApp/.env
Normal file
1
active-allocator/ClientApp/.env
Normal file
@ -0,0 +1 @@
|
||||
BROWSER=none
|
3
active-allocator/ClientApp/.env.development
Normal file
3
active-allocator/ClientApp/.env.development
Normal file
@ -0,0 +1,3 @@
|
||||
PORT=44423
|
||||
HTTPS=true
|
||||
|
21
active-allocator/ClientApp/.gitignore
vendored
Normal file
21
active-allocator/ClientApp/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
2228
active-allocator/ClientApp/README.md
Normal file
2228
active-allocator/ClientApp/README.md
Normal file
File diff suppressed because it is too large
Load Diff
33
active-allocator/ClientApp/aspnetcore-https.js
Normal file
33
active-allocator/ClientApp/aspnetcore-https.js
Normal file
@ -0,0 +1,33 @@
|
||||
// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate
|
||||
const fs = require('fs');
|
||||
const spawn = require('child_process').spawn;
|
||||
const path = require('path');
|
||||
|
||||
const baseFolder =
|
||||
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
|
||||
? `${process.env.APPDATA}/ASP.NET/https`
|
||||
: `${process.env.HOME}/.aspnet/https`;
|
||||
|
||||
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
|
||||
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
|
||||
|
||||
if (!certificateName) {
|
||||
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
spawn('dotnet', [
|
||||
'dev-certs',
|
||||
'https',
|
||||
'--export-path',
|
||||
certFilePath,
|
||||
'--format',
|
||||
'Pem',
|
||||
'--no-password',
|
||||
], { stdio: 'inherit', })
|
||||
.on('exit', (code) => process.exit(code));
|
||||
}
|
55
active-allocator/ClientApp/aspnetcore-react.js
Normal file
55
active-allocator/ClientApp/aspnetcore-react.js
Normal file
@ -0,0 +1,55 @@
|
||||
// This script configures the .env.development.local file with additional environment variables to configure HTTPS using the ASP.NET Core
|
||||
// development certificate in the webpack development proxy.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const baseFolder =
|
||||
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
|
||||
? `${process.env.APPDATA}/ASP.NET/https`
|
||||
: `${process.env.HOME}/.aspnet/https`;
|
||||
|
||||
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
|
||||
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
|
||||
|
||||
if (!certificateName) {
|
||||
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync('.env.development.local')) {
|
||||
fs.writeFileSync(
|
||||
'.env.development.local',
|
||||
`SSL_CRT_FILE=${certFilePath}
|
||||
SSL_KEY_FILE=${keyFilePath}`
|
||||
);
|
||||
} else {
|
||||
let lines = fs.readFileSync('.env.development.local')
|
||||
.toString()
|
||||
.split('\n');
|
||||
|
||||
let hasCert, hasCertKey = false;
|
||||
for (const line of lines) {
|
||||
if (/SSL_CRT_FILE=.*/i.test(line)) {
|
||||
hasCert = true;
|
||||
}
|
||||
if (/SSL_KEY_FILE=.*/i.test(line)) {
|
||||
hasCertKey = true;
|
||||
}
|
||||
}
|
||||
if (!hasCert) {
|
||||
fs.appendFileSync(
|
||||
'.env.development.local',
|
||||
`\nSSL_CRT_FILE=${certFilePath}`
|
||||
);
|
||||
}
|
||||
if (!hasCertKey) {
|
||||
fs.appendFileSync(
|
||||
'.env.development.local',
|
||||
`\nSSL_KEY_FILE=${keyFilePath}`
|
||||
);
|
||||
}
|
||||
}
|
29519
active-allocator/ClientApp/package-lock.json
generated
Normal file
29519
active-allocator/ClientApp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
active-allocator/ClientApp/package.json
Normal file
74
active-allocator/ClientApp/package.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "active_allocator",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.2.3",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"jquery": "^3.6.4",
|
||||
"merge": "^2.1.1",
|
||||
"oidc-client": "^1.11.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-bootstrap": "^0.26.2",
|
||||
"react-router-dom": "^6.11.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"reactstrap": "^9.1.9",
|
||||
"rimraf": "^5.0.0",
|
||||
"web-vitals": "^3.3.1",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
"workbox-broadcast-update": "^6.5.4",
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
"workbox-core": "^6.5.4",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-google-analytics": "^6.5.4",
|
||||
"workbox-navigation-preload": "^6.5.4",
|
||||
"workbox-precaching": "^6.5.4",
|
||||
"workbox-range-requests": "^6.5.4",
|
||||
"workbox-routing": "^6.5.4",
|
||||
"workbox-strategies": "^6.5.4",
|
||||
"workbox-streams": "^6.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"nan": "^2.17.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"overrides": {
|
||||
"autoprefixer": "10.4.5",
|
||||
"nth-check": "2.1.1",
|
||||
"webpack": "5.81.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "node aspnetcore-https && node aspnetcore-react",
|
||||
"start": "rimraf ./build && react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "cross-env CI=true react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint ./src/"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
active-allocator/ClientApp/public/favicon.ico
Normal file
BIN
active-allocator/ClientApp/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
41
active-allocator/ClientApp/public/index.html
Normal file
41
active-allocator/ClientApp/public/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<base href="%PUBLIC_URL%/" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>active_allocator</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
15
active-allocator/ClientApp/public/manifest.json
Normal file
15
active-allocator/ClientApp/public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "active_allocator",
|
||||
"name": "active_allocator",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
22
active-allocator/ClientApp/src/App.js
Normal file
22
active-allocator/ClientApp/src/App.js
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import { Layout } from './components/Layout';
|
||||
import './custom.css';
|
||||
|
||||
export default class App extends Component {
|
||||
static displayName = App.name;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
{AppRoutes.map((route, index) => {
|
||||
const { element, ...rest } = route;
|
||||
return <Route key={index} {...rest} element={element} />;
|
||||
})}
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
14
active-allocator/ClientApp/src/App.test.js
Normal file
14
active-allocator/ClientApp/src/App.test.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', async () => {
|
||||
const div = document.createElement('div');
|
||||
const root = createRoot(div);
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<App />
|
||||
</MemoryRouter>);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
20
active-allocator/ClientApp/src/AppRoutes.js
Normal file
20
active-allocator/ClientApp/src/AppRoutes.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { Counter } from "./components/Counter";
|
||||
import { FetchData } from "./components/FetchData";
|
||||
import { Home } from "./components/Home";
|
||||
|
||||
const AppRoutes = [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: '/counter',
|
||||
element: <Counter />
|
||||
},
|
||||
{
|
||||
path: '/fetch-data',
|
||||
element: <FetchData />
|
||||
}
|
||||
];
|
||||
|
||||
export default AppRoutes;
|
31
active-allocator/ClientApp/src/components/Counter.js
Normal file
31
active-allocator/ClientApp/src/components/Counter.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class Counter extends Component {
|
||||
static displayName = Counter.name;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { currentCount: 0 };
|
||||
this.incrementCounter = this.incrementCounter.bind(this);
|
||||
}
|
||||
|
||||
incrementCounter() {
|
||||
this.setState({
|
||||
currentCount: this.state.currentCount + 1
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p>This is a simple example of a React component.</p>
|
||||
|
||||
<p aria-live="polite">Current count: <strong>{this.state.currentCount}</strong></p>
|
||||
|
||||
<button className="btn btn-primary" onClick={this.incrementCounter}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
59
active-allocator/ClientApp/src/components/FetchData.js
Normal file
59
active-allocator/ClientApp/src/components/FetchData.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class FetchData extends Component {
|
||||
static displayName = FetchData.name;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { forecasts: [], loading: true };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.populateWeatherData();
|
||||
}
|
||||
|
||||
static renderForecastsTable(forecasts) {
|
||||
return (
|
||||
<table className="table table-striped" aria-labelledby="tableLabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forecasts.map(forecast =>
|
||||
<tr key={forecast.date}>
|
||||
<td>{forecast.date}</td>
|
||||
<td>{forecast.temperatureC}</td>
|
||||
<td>{forecast.temperatureF}</td>
|
||||
<td>{forecast.summary}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let contents = this.state.loading
|
||||
? <p><em>Loading...</em></p>
|
||||
: FetchData.renderForecastsTable(this.state.forecasts);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 id="tableLabel">Weather forecast</h1>
|
||||
<p>This component demonstrates fetching data from the server.</p>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async populateWeatherData() {
|
||||
const response = await fetch('weatherforecast');
|
||||
const data = await response.json();
|
||||
this.setState({ forecasts: data, loading: false });
|
||||
}
|
||||
}
|
26
active-allocator/ClientApp/src/components/Home.js
Normal file
26
active-allocator/ClientApp/src/components/Home.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class Home extends Component {
|
||||
static displayName = Home.name;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, world!</h1>
|
||||
<p>Welcome to your new single-page application, built with:</p>
|
||||
<ul>
|
||||
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
|
||||
<li><a href='https://facebook.github.io/react/'>React</a> for client-side code</li>
|
||||
<li><a href='http://getbootstrap.com/'>Bootstrap</a> for layout and styling</li>
|
||||
</ul>
|
||||
<p>To help you get started, we have also set up:</p>
|
||||
<ul>
|
||||
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
|
||||
<li><strong>Development server integration</strong>. In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.</li>
|
||||
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files.</li>
|
||||
</ul>
|
||||
<p>The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>npm</code> commands such as <code>npm test</code> or <code>npm install</code>.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
18
active-allocator/ClientApp/src/components/Layout.js
Normal file
18
active-allocator/ClientApp/src/components/Layout.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Container } from 'reactstrap';
|
||||
import { NavMenu } from './NavMenu';
|
||||
|
||||
export class Layout extends Component {
|
||||
static displayName = Layout.name;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavMenu />
|
||||
<Container tag="main">
|
||||
{this.props.children}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
18
active-allocator/ClientApp/src/components/NavMenu.css
Normal file
18
active-allocator/ClientApp/src/components/NavMenu.css
Normal file
@ -0,0 +1,18 @@
|
||||
a.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||
}
|
47
active-allocator/ClientApp/src/components/NavMenu.js
Normal file
47
active-allocator/ClientApp/src/components/NavMenu.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Collapse, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './NavMenu.css';
|
||||
|
||||
export class NavMenu extends Component {
|
||||
static displayName = NavMenu.name;
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.toggleNavbar = this.toggleNavbar.bind(this);
|
||||
this.state = {
|
||||
collapsed: true
|
||||
};
|
||||
}
|
||||
|
||||
toggleNavbar () {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header>
|
||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm ng-white border-bottom box-shadow mb-3" container light>
|
||||
<NavbarBrand tag={Link} to="/">active_allocator</NavbarBrand>
|
||||
<NavbarToggler onClick={this.toggleNavbar} className="mr-2" />
|
||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={!this.state.collapsed} navbar>
|
||||
<ul className="navbar-nav flex-grow">
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/">Home</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/counter">Counter</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} className="text-dark" to="/fetch-data">Fetch data</NavLink>
|
||||
</NavItem>
|
||||
</ul>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
18
active-allocator/ClientApp/src/custom.css
Normal file
18
active-allocator/ClientApp/src/custom.css
Normal file
@ -0,0 +1,18 @@
|
||||
/* Provide sufficient contrast against white background */
|
||||
a {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #E01A76;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
26
active-allocator/ClientApp/src/index.js
Normal file
26
active-allocator/ClientApp/src/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
|
||||
const rootElement = document.getElementById('root');
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<BrowserRouter basename={baseUrl}>
|
||||
<App />
|
||||
</BrowserRouter>);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://cra.link/PWA
|
||||
serviceWorkerRegistration.unregister();
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
13
active-allocator/ClientApp/src/reportWebVitals.js
Normal file
13
active-allocator/ClientApp/src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
||||
const reportWebVitals = (onPerfEntry) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
72
active-allocator/ClientApp/src/service-worker.js
Normal file
72
active-allocator/ClientApp/src/service-worker.js
Normal file
@ -0,0 +1,72 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// This service worker can be customized!
|
||||
// See https://developers.google.com/web/tools/workbox/modules
|
||||
// for the list of available Workbox modules, or add any other
|
||||
// code you'd like.
|
||||
// You can also remove this file if you'd prefer not to use a
|
||||
// service worker, and the Workbox build step will be skipped.
|
||||
|
||||
import { clientsClaim } from 'workbox-core';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { StaleWhileRevalidate } from 'workbox-strategies';
|
||||
|
||||
clientsClaim();
|
||||
|
||||
// Precache all of the assets generated by your build process.
|
||||
// Their URLs are injected into the manifest variable below.
|
||||
// This variable must be present somewhere in your service worker file,
|
||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
||||
registerRoute(
|
||||
// Return false to exempt requests from being fulfilled by index.html.
|
||||
({ request, url }) => {
|
||||
// If this isn't a navigation, skip.
|
||||
if (request.mode !== 'navigate') {
|
||||
return false;
|
||||
} // If this is a URL that starts with /_, skip.
|
||||
|
||||
if (url.pathname.startsWith('/_')) {
|
||||
return false;
|
||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||
|
||||
if (url.pathname.match(fileExtensionRegexp)) {
|
||||
return false;
|
||||
} // Return true to signal that we want to use the handler.
|
||||
|
||||
return true;
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
);
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
// Ensure that once this runtime cache reaches a maximum size the
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 }),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
// Any other custom service worker logic can go here.
|
137
active-allocator/ClientApp/src/serviceWorkerRegistration.js
Normal file
137
active-allocator/ClientApp/src/serviceWorkerRegistration.js
Normal file
@ -0,0 +1,137 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://cra.link/PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
31
active-allocator/ClientApp/src/setupProxy.js
Normal file
31
active-allocator/ClientApp/src/setupProxy.js
Normal file
@ -0,0 +1,31 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const { env } = require('process');
|
||||
|
||||
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
||||
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:41950';
|
||||
|
||||
const context = [
|
||||
"/weatherforecast",
|
||||
];
|
||||
|
||||
const onError = (err, req, resp, target) => {
|
||||
console.error(`${err.message}`);
|
||||
}
|
||||
|
||||
module.exports = function (app) {
|
||||
const appProxy = createProxyMiddleware(context, {
|
||||
proxyTimeout: 10000,
|
||||
target: target,
|
||||
// Handle errors to prevent the proxy middleware from crashing when
|
||||
// the ASP NET Core webserver is unavailable
|
||||
onError: onError,
|
||||
secure: false,
|
||||
// Uncomment this line to add support for proxying websockets
|
||||
//ws: true,
|
||||
headers: {
|
||||
Connection: 'Keep-Alive'
|
||||
}
|
||||
});
|
||||
|
||||
app.use(appProxy);
|
||||
};
|
73
active-allocator/Controllers/UserController.cs
Normal file
73
active-allocator/Controllers/UserController.cs
Normal file
@ -0,0 +1,73 @@
|
||||
namespace active_allocator.Controllers;
|
||||
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using active_allocator.Models.Users;
|
||||
using active_allocator.Services;
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private IUserService _userService;
|
||||
private IMapper _mapper;
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public UsersController(
|
||||
IUserService userService,
|
||||
IMapper mapper,
|
||||
IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_appSettings = appSettings.Value;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("authenticate")]
|
||||
public IActionResult Authenticate(AuthenticateRequest model)
|
||||
{
|
||||
var response = _userService.Authenticate(model);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("register")]
|
||||
public IActionResult Register(RegisterRequest model)
|
||||
{
|
||||
_userService.Register(model);
|
||||
return Ok(new { message = "Registration successful" });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetAll()
|
||||
{
|
||||
var users = _userService.GetAll();
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetById(int id)
|
||||
{
|
||||
var user = _userService.GetById(id);
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public IActionResult Update(int id, [FromBody]UpdateRequest model)
|
||||
{
|
||||
_userService.Update(id, model);
|
||||
return Ok(new { message = "User updated" });
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public IActionResult Delete(int id)
|
||||
{
|
||||
_userService.Delete(id);
|
||||
return Ok(new { message = "User deleted" });
|
||||
}
|
||||
}
|
32
active-allocator/Controllers/WeatherForecastController.cs
Normal file
32
active-allocator/Controllers/WeatherForecastController.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace active_allocator.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
7
active-allocator/Entities/Role.cs
Normal file
7
active-allocator/Entities/Role.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace active_allocator.Entities;
|
||||
|
||||
public enum Role
|
||||
{
|
||||
Admin,
|
||||
User
|
||||
}
|
16
active-allocator/Entities/User.cs
Normal file
16
active-allocator/Entities/User.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace active_allocator.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public Role Role { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string PasswordHash { get; set; }
|
||||
}
|
17
active-allocator/Helpers/AppException.cs
Normal file
17
active-allocator/Helpers/AppException.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
// custom exception class for throwing application specific exceptions (e.g. for validation)
|
||||
// that can be caught and handled within the application
|
||||
public class AppException : Exception
|
||||
{
|
||||
public AppException() : base() {}
|
||||
|
||||
public AppException(string message) : base(message) { }
|
||||
|
||||
public AppException(string message, params object[] args)
|
||||
: base(String.Format(CultureInfo.CurrentCulture, message, args))
|
||||
{
|
||||
}
|
||||
}
|
6
active-allocator/Helpers/AppSettings.cs
Normal file
6
active-allocator/Helpers/AppSettings.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public required string Secret { get; set; }
|
||||
}
|
33
active-allocator/Helpers/AutoMapperProfile.cs
Normal file
33
active-allocator/Helpers/AutoMapperProfile.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using AutoMapper;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Models.Users;
|
||||
|
||||
public class AutoMapperProfile : Profile
|
||||
{
|
||||
public AutoMapperProfile()
|
||||
{
|
||||
// User -> AuthenticateResponse
|
||||
CreateMap<User, AuthenticateResponse>();
|
||||
|
||||
// RegisterRequest -> User
|
||||
CreateMap<RegisterRequest, User>();
|
||||
|
||||
// UpdateRequest -> User
|
||||
CreateMap<UpdateRequest, User>()
|
||||
.ForAllMembers(x => x.Condition(
|
||||
(src, dest, prop) =>
|
||||
{
|
||||
// ignore both null & empty string properties
|
||||
if (prop == null) return false;
|
||||
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
|
||||
|
||||
// ignore null role
|
||||
if (x.DestinationMember.Name == "Role" && src.Role == null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
22
active-allocator/Helpers/DataContext.cs
Normal file
22
active-allocator/Helpers/DataContext.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class DataContext : DbContext
|
||||
{
|
||||
//protected readonly IConfiguration Configuration;
|
||||
|
||||
public DataContext(DbContextOptions<DataContext> options) : base(options)
|
||||
{
|
||||
//Configuration = configuration;
|
||||
}
|
||||
|
||||
/*protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
{
|
||||
// in memory database used for simplicity, change to a real db for production applications
|
||||
options.UseInMemoryDatabase("TestDb");
|
||||
}*/
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
}
|
54
active-allocator/Helpers/ErrorHandlerMiddleware.cs
Normal file
54
active-allocator/Helpers/ErrorHandlerMiddleware.cs
Normal file
@ -0,0 +1,54 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class ErrorHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
var response = context.Response;
|
||||
response.ContentType = "application/json";
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case AppException e:
|
||||
// custom application error
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
break;
|
||||
case KeyNotFoundException e:
|
||||
// not found error
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
break;
|
||||
default:
|
||||
// unhandled error
|
||||
_logger.LogError(error, error.Message);
|
||||
response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
break;
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Serialize(new { message = error?.Message });
|
||||
await response.WriteAsync(result);
|
||||
}
|
||||
}
|
||||
}
|
65
active-allocator/Migrations/20231117033912_InitialCreate.Designer.cs
generated
Normal file
65
active-allocator/Migrations/20231117033912_InitialCreate.Designer.cs
generated
Normal file
@ -0,0 +1,65 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace active_allocator.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20231117033912_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("active_allocator.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
40
active-allocator/Migrations/20231117033912_InitialCreate.cs
Normal file
40
active-allocator/Migrations/20231117033912_InitialCreate.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace active_allocator.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Username = table.Column<string>(type: "text", nullable: false),
|
||||
FirstName = table.Column<string>(type: "text", nullable: false),
|
||||
LastName = table.Column<string>(type: "text", nullable: false),
|
||||
Email = table.Column<string>(type: "text", nullable: false),
|
||||
Role = table.Column<int>(type: "integer", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
62
active-allocator/Migrations/DataContextModelSnapshot.cs
Normal file
62
active-allocator/Migrations/DataContextModelSnapshot.cs
Normal file
@ -0,0 +1,62 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace active_allocator.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
partial class DataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "7.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("active_allocator.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
12
active-allocator/Models/Users/AuthenticateRequest.cs
Normal file
12
active-allocator/Models/Users/AuthenticateRequest.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class AuthenticateRequest
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; }
|
||||
}
|
10
active-allocator/Models/Users/AuthenticateResponse.cs
Normal file
10
active-allocator/Models/Users/AuthenticateResponse.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
public class AuthenticateResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
28
active-allocator/Models/Users/RegisterRequest.cs
Normal file
28
active-allocator/Models/Users/RegisterRequest.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[Required]
|
||||
public string LastName { get; set; }
|
||||
|
||||
[Required]
|
||||
[EnumDataType(typeof(Role))]
|
||||
public string Role { get; set; }
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(6)]
|
||||
public string Password { get; set; }
|
||||
}
|
35
active-allocator/Models/Users/UpdateRequest.cs
Normal file
35
active-allocator/Models/Users/UpdateRequest.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class UpdateRequest
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
|
||||
[EnumDataType(typeof(Role))]
|
||||
public string Role { get; set; }
|
||||
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
// treat empty string as null for password fields to
|
||||
// make them optional in front end apps
|
||||
private string _password;
|
||||
[MinLength(6)]
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => _password = replaceEmptyWithNull(value);
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private string replaceEmptyWithNull(string value)
|
||||
{
|
||||
// replace empty string with null to make field optional
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
}
|
26
active-allocator/Pages/Error.cshtml
Normal file
26
active-allocator/Pages/Error.cshtml
Normal file
@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<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>
|
25
active-allocator/Pages/Error.cshtml.cs
Normal file
25
active-allocator/Pages/Error.cshtml.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace active_allocator.Pages;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
3
active-allocator/Pages/_ViewImports.cshtml
Normal file
3
active-allocator/Pages/_ViewImports.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@using active_allocator
|
||||
@namespace active_allocator.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
62
active-allocator/Program.cs
Normal file
62
active-allocator/Program.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using active_allocator.Services;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AA API", Version = "v1" });
|
||||
});
|
||||
|
||||
builder.Services.AddAutoMapper(typeof(Program));
|
||||
builder.Services.AddDbContext<DataContext>(opt =>
|
||||
opt.UseNpgsql(builder.Configuration.GetConnectionString("PSQLConnection")));
|
||||
|
||||
// Configure strongly typed settings object
|
||||
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
|
||||
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// 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.UseStaticFiles();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseRouting();
|
||||
|
||||
// custom jwt auth middleware
|
||||
app.UseMiddleware<JwtMiddleware>();
|
||||
|
||||
// global error handler
|
||||
app.UseMiddleware<ErrorHandlerMiddleware>();
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
29
active-allocator/Properties/launchSettings.json
Normal file
29
active-allocator/Properties/launchSettings.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:41950",
|
||||
"sslPort": 44372
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"active_allocator": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7260;http://localhost:5135",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
active-allocator/Services/UserService.cs
Normal file
114
active-allocator/Services/UserService.cs
Normal file
@ -0,0 +1,114 @@
|
||||
namespace active_allocator.Services;
|
||||
|
||||
using AutoMapper;
|
||||
using BCrypt.Net;
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Helpers;
|
||||
using active_allocator.Models.Users;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
AuthenticateResponse Authenticate(AuthenticateRequest model);
|
||||
void Register(RegisterRequest model);
|
||||
IEnumerable<User> GetAll();
|
||||
User GetById(int id);
|
||||
void Update(int id, UpdateRequest model);
|
||||
void Delete(int id);
|
||||
}
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private DataContext _context;
|
||||
private IJwtUtils _jwtUtils;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserService(
|
||||
DataContext context,
|
||||
IMapper mapper,
|
||||
IJwtUtils jwtUtils)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_jwtUtils = jwtUtils;
|
||||
}
|
||||
|
||||
public AuthenticateResponse Authenticate(AuthenticateRequest model)
|
||||
{
|
||||
var user = _context.Users.SingleOrDefault(x => x.Username == model.Username);
|
||||
|
||||
// validate
|
||||
if (user == null || !BCrypt.Verify(model.Password, user.PasswordHash))
|
||||
throw new AppException("Username or password is incorrect");
|
||||
|
||||
// authentication successful
|
||||
var response = _mapper.Map<AuthenticateResponse>(user);
|
||||
response.Token = _jwtUtils.GenerateToken(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
public void Register(RegisterRequest model)
|
||||
{
|
||||
// validate username
|
||||
if (_context.Users.Any(x => x.Username == model.Username))
|
||||
throw new AppException("Username '" + model.Username + "' is already taken");
|
||||
|
||||
// validate email
|
||||
if (_context.Users.Any(x => x.Email == model.Email))
|
||||
throw new AppException("Email '" + model.Email + "' is already taken");
|
||||
|
||||
// map model to new user object
|
||||
var user = _mapper.Map<User>(model);
|
||||
|
||||
// hash password
|
||||
user.PasswordHash = BCrypt.HashPassword(model.Password);
|
||||
|
||||
// save user
|
||||
_context.Users.Add(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetAll()
|
||||
{
|
||||
return _context.Users;
|
||||
}
|
||||
|
||||
public User GetById(int id)
|
||||
{
|
||||
return getUser(id);
|
||||
}
|
||||
|
||||
public void Update(int id, UpdateRequest model)
|
||||
{
|
||||
var user = getUser(id);
|
||||
|
||||
// validate
|
||||
if (model.Email != user.Email && _context.Users.Any(x => x.Email == model.Email))
|
||||
throw new AppException("User with the email '" + model.Email + "' already exists");
|
||||
|
||||
// hash password if it was entered
|
||||
if (!string.IsNullOrEmpty(model.Password))
|
||||
user.PasswordHash = BCrypt.HashPassword(model.Password);
|
||||
|
||||
// copy model to user and save
|
||||
_mapper.Map(model, user);
|
||||
_context.Users.Update(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
var user = getUser(id);
|
||||
_context.Users.Remove(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
private User getUser(int id)
|
||||
{
|
||||
var user = _context.Users.Find(id);
|
||||
if (user == null) throw new KeyNotFoundException("User not found");
|
||||
return user;
|
||||
}
|
||||
}
|
12
active-allocator/WeatherForecast.cs
Normal file
12
active-allocator/WeatherForecast.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace active_allocator;
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
75
active-allocator/active-allocator.csproj
Normal file
75
active-allocator/active-allocator.csproj
Normal file
@ -0,0 +1,75 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SpaRoot>ClientApp\</SpaRoot>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
||||
<SpaProxyServerUrl>https://localhost:44423</SpaProxyServerUrl>
|
||||
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
|
||||
<RootNamespace>active_allocator</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ContainerImageName>activeallocator</ContainerImageName>
|
||||
<PublishProfile>DefaultContainer</PublishProfile>
|
||||
<ContainerImageTags>1.0.0;latest</ContainerImageTags>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="BCrypt.Net" Version="0.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.32.1" />
|
||||
<PackageReference Include="Microsoft.NET.Build.Containers" Version="7.0.400" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.13" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||
<Content Remove="$(SpaRoot)**" />
|
||||
<None Remove="$(SpaRoot)**" />
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||
<!-- Ensure Node.js is installed -->
|
||||
<Exec Command="node --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
||||
</Exec>
|
||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
||||
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
|
||||
|
||||
<!-- Include the newly-built files in the publish output -->
|
||||
<ItemGroup>
|
||||
<DistFiles Include="$(SpaRoot)build\**" />
|
||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
10
active-allocator/appsettings.Development.json
Normal file
10
active-allocator/appsettings.Development.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.AspNetCore.SpaProxy": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
16
active-allocator/appsettings.json
Normal file
16
active-allocator/appsettings.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"PSQLConnection": "Server=localhost;Port=15432;Database=aadb;User Id=postgres;Password=nqA3UV3CliLLHpLL"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AppSettings": {
|
||||
"Secret": "5de80277015f9fd564c4d1cc2cf827dbb1774cd66e7d79aa258d9c35a9f67f32fc6cf0dc24244242bd9501288e0fd69e315b"
|
||||
}
|
||||
}
|
11
active-allocator/docker-compose.yml
Normal file
11
active-allocator/docker-compose.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
activeallocator-site:
|
||||
image: activeallocator:1.0.0
|
||||
container_name: activeallocator
|
||||
#build:
|
||||
#context: .
|
||||
#dockerfile: MoviesAPI/Dockerfile
|
||||
ports:
|
||||
- 80:80
|
Loading…
x
Reference in New Issue
Block a user