Initial project
This commit is contained in:
84
CLAUDE.md
Normal file
84
CLAUDE.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# CLAUDE.md - SoaFE Tournament Portal
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This application is a tournament organization portal for the Secrets of a Fallen Empire (SoaFE) mod for Star Wars: Empire at War: Forces of Corruption.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- .NET 10, Blazor Interactive WebAssembly
|
||||||
|
- Entity Framework Core 10 with PostgreSQL
|
||||||
|
- Radzen Blazor Components for UI layout and components (MCP server: `radzen.mcp`)
|
||||||
|
- FluentValidation for validating models
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- `src/Client/` -- Client-side components, pages, and layouts
|
||||||
|
- `src/Core/` -- Abstractions, components, and utilities used by both `src/Client/` and `src/Server`
|
||||||
|
- `src/Data/` -- Entity Framework Core, ORM models, DB functions, migrations, and enums
|
||||||
|
- `src/Server/` -- API, Server-side components, pages, and services
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- Build: `dotnet build`
|
||||||
|
- Run: `dotnet run --project src/Server`
|
||||||
|
- Add Migration: `dotnet ef migrations add <Name> -p "src/Data" -s "src/Server"`
|
||||||
|
- Update Database: `dotnet ef database update -p "src/Data" -s "src/Server"`
|
||||||
|
- Format: `dotnet format`
|
||||||
|
|
||||||
|
## Architecture Rules
|
||||||
|
- The `src/Client/` WebAssembly project houses most all layouts, pages, and components
|
||||||
|
- The `src/Server/` project houses server-side rendered components and API endpoints
|
||||||
|
- The `src/Core/` project houses reusable component abstractions, controls, and services that are frequently reused throughout the solution
|
||||||
|
- When designing views, use `#radzen_search` to search the Radzen UI component documentation
|
||||||
|
- The `src/Data/` project contains all of the Entity Framework Core models, database contexts, function definitions, view definitions, and migration scripts
|
||||||
|
- All database access goes through an Entity Framework Core DbContext without repository pattern
|
||||||
|
- All .NET NuGet packages should use version `$(DotNetVersion)` (defined in `Directory.Build.props`) where available, with a fallback to the latest compatible version
|
||||||
|
- UI strings are defined in resource files (`.resx`) -- no hard coding of any strings that are displayed on the interface
|
||||||
|
- Major app features are organized into `Features/<Name>/` with the following organization substructure:
|
||||||
|
- `Dialogs/` -- Components that are only meant to be used in a dialog modal
|
||||||
|
- `Extensions/` -- Contains extensions (i.e. `ServiceCollectionExtensions.cs`) to extend the application to use services within this feature
|
||||||
|
- `Models/` -- Record type models used by the views in this feature
|
||||||
|
- `Pages/` -- Routable components
|
||||||
|
- `Services/` -- DI Services (i.e. validators) used by this feature
|
||||||
|
- `Shared/` -- Non-routable or dialog components used by this feature
|
||||||
|
- Layout components are implemented in `Layout`
|
||||||
|
- Root pages (e.g. home page) are implemented in `Features/Home`
|
||||||
|
- Shared components across all features are implemented in `Features/Shared`
|
||||||
|
- When writing component code-behind files, organize in the following order
|
||||||
|
1. Parameters Region (public parameters, private cascading parameters, private dependency injections)
|
||||||
|
2. State Region (private state fields, private or protected lambdas)
|
||||||
|
3. Lifecycle Region (SetParametersAsync, OnInitialized/OnInitializedAsync, OnParametersSet/OnParametersSetAsync, OnFirstRender/OnFirstRenderAsync, Dispose)
|
||||||
|
4. Mutators Region (any private methods that modifies component state or performs an action)
|
||||||
|
5. Callbacks Region (any private methods that are wired up to controls as an EventCallback; callbacks should only call a mutator)
|
||||||
|
6. Anything else (refrain from using public methods, but put them at the bottom if necessary)
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
- Routable pages: `[Name]Index`, `[Name]Detail`
|
||||||
|
- Dialogs: `[Name]Dialog`
|
||||||
|
- Components: `[Name]Form`, `[Name]Grid`, `[Name]DropDown`
|
||||||
|
- DTO Models: `[Name]Model`
|
||||||
|
- Validators: `[Name]Validator`
|
||||||
|
- Component code-behind: `[Component].razor.cs`
|
||||||
|
- Enums: `E[Name].cs`
|
||||||
|
|
||||||
|
### Patterns We Use
|
||||||
|
- DTO models are record type and inherit `Feature`
|
||||||
|
- File-scoped namespaces
|
||||||
|
- Always pass `CancellationToken` to async methods where available
|
||||||
|
- Code-behind files for Blazor components; `@code` blocks in `.razor` files only for reusable RenderFragments
|
||||||
|
- Resource `.resx` files for UI strings unique to components
|
||||||
|
- `_Imports.razor` file with a `@namespace` definition for any folder that contains components
|
||||||
|
- ORM models implement `IEntityTypeConfiguration<T>` and define explicit column names and ordering consistent with PostgreSQL naming conventions
|
||||||
|
- Explicit projection of ORM models into DTO models
|
||||||
|
- One file per type
|
||||||
|
|
||||||
|
### Patters We DO NOT Use
|
||||||
|
- Primary constructors
|
||||||
|
- Repository patterns
|
||||||
|
- AutoMapper, Mapster, etc.
|
||||||
|
- Hard-coded strings for UI elements
|
||||||
|
- Multiple types in a file or nested classes
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- All feature models should have a validator that inherits `AbstractValidator<T>` with validation rules for applicable fields
|
||||||
|
- Validators are automatically ran by forms including the `<FluentValidator />` component
|
||||||
|
- Validators that include async rules must use `<FluentValidator AsyncMode />` in edit forms
|
||||||
12
Directory.Build.props
Normal file
12
Directory.Build.props
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<DotNetVersion>10.0.*</DotNetVersion>
|
||||||
|
<Namespace>SoaFE.Portal</Namespace>
|
||||||
|
<Authors>Drew Cavanaugh</Authors>
|
||||||
|
<Company />
|
||||||
|
<Product>SoaFE Portal</Product>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
6
SoaFE Portal.slnx
Normal file
6
SoaFE Portal.slnx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="src/Client/Client.csproj" />
|
||||||
|
<Project Path="src/Core/Core.csproj" />
|
||||||
|
<Project Path="src/Data/Data.csproj" />
|
||||||
|
<Project Path="src/Server/Server.csproj" />
|
||||||
|
</Solution>
|
||||||
52
SoaFE-Portal.code-workspace
Normal file
52
SoaFE-Portal.code-workspace
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"dotnet.defaultSolution": "SoaFE Portal.slnx"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/src/Server/Server.csproj"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile",
|
||||||
|
"group": "build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"launch": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug (ENV: Dev)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/src/Server/bin/Debug/net10.0/SoaFE.Portal.Server.dll",
|
||||||
|
"cwd": "${workspaceFolder}/src/Server",
|
||||||
|
"args": [],
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_USE_POLLING_FILE_WATCHER": "true",
|
||||||
|
"DOTNET_HOTRELOAD": "1"
|
||||||
|
},
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"serverReadyAction": {
|
||||||
|
"pattern": "Now listening on:\\s+(https?://\\S+)",
|
||||||
|
"uriFormat": "%s",
|
||||||
|
"action": "openExternally"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Client/Client.csproj
Normal file
17
src/Client/Client.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>$(Namespace).Client</RootNamespace>
|
||||||
|
<AssemblyName>$(Namespace).Client</AssemblyName>
|
||||||
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
|
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||||
|
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Core/Core.csproj" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(DotNetVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
7
src/Client/Features/Home/Pages/Home.razor
Normal file
7
src/Client/Features/Home/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@page "/"
|
||||||
|
|
||||||
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
|
Welcome to your new app.
|
||||||
5
src/Client/Features/Home/Pages/NotFound.razor
Normal file
5
src/Client/Features/Home/Pages/NotFound.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@page "/not-found"
|
||||||
|
@layout MainLayout
|
||||||
|
|
||||||
|
<h3>Not Found</h3>
|
||||||
|
<p>Sorry, the content you are looking for does not exist.</p>
|
||||||
1
src/Client/Features/Home/Pages/_Imports.razor
Normal file
1
src/Client/Features/Home/Pages/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Client.Features.Home.Pages
|
||||||
1
src/Client/Features/Home/_Imports.razor
Normal file
1
src/Client/Features/Home/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Client.Features.Home
|
||||||
1
src/Client/Features/_Imports.razor
Normal file
1
src/Client/Features/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Client.Features
|
||||||
20
src/Client/Layout/EmptyLayout.razor
Normal file
20
src/Client/Layout/EmptyLayout.razor
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
@inject VersionProvider VersionProvider
|
||||||
|
|
||||||
|
<RadzenLayout>
|
||||||
|
<RadzenBody>
|
||||||
|
<AlertErrorBoundary>
|
||||||
|
@Body
|
||||||
|
</AlertErrorBoundary>
|
||||||
|
</RadzenBody>
|
||||||
|
<RadzenFooter>
|
||||||
|
<RadzenText class="rz-py-2 rz-mt-auto"
|
||||||
|
TextStyle=TextStyle.Subtitle2
|
||||||
|
TextAlign=TextAlign.Center>
|
||||||
|
v.
|
||||||
|
@VersionProvider.Version
|
||||||
|
</RadzenText>
|
||||||
|
</RadzenFooter>
|
||||||
|
</RadzenLayout>
|
||||||
|
|
||||||
|
<RadzenComponents />
|
||||||
44
src/Client/Layout/MainLayout.cs
Normal file
44
src/Client/Layout/MainLayout.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using SoaFE.Portal.Core.Services;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Client.Layout;
|
||||||
|
|
||||||
|
public partial class MainLayout
|
||||||
|
{
|
||||||
|
#region Parameters
|
||||||
|
|
||||||
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
[Inject] private IStringLocalizer<MainLayout> Strings { get; set; } = default!;
|
||||||
|
[Inject] private VersionProvider VersionProvider { get; set; } = default!;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
private bool _isSidebarCollapsed;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mutators
|
||||||
|
|
||||||
|
private void ToggleSidebar()
|
||||||
|
{
|
||||||
|
_isSidebarCollapsed = !_isSidebarCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Callbacks
|
||||||
|
|
||||||
|
private void ToggleSidebarOnClick()
|
||||||
|
=> ToggleSidebar();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
45
src/Client/Layout/MainLayout.razor
Normal file
45
src/Client/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<RadzenLayout Style="grid-template-areas: 'rz-sidebar rz-header' 'rz-sidebar rz-body';">
|
||||||
|
<RadzenSidebar Style="width: max-content;"
|
||||||
|
Responsive=false>
|
||||||
|
<RadzenHeader>
|
||||||
|
<RadzenStack Orientation=Orientation.Horizontal
|
||||||
|
AlignItems=AlignItems.Center
|
||||||
|
Gap="0">
|
||||||
|
<RadzenSidebarToggle class="rz-me-0 rz-border-0"
|
||||||
|
Click=ToggleSidebarOnClick />
|
||||||
|
<RadzenText TextStyle=TextStyle.H5
|
||||||
|
Text=@Strings["AppTitle"]
|
||||||
|
Visible=!_isSidebarCollapsed />
|
||||||
|
</RadzenStack>
|
||||||
|
</RadzenHeader>
|
||||||
|
<NavMenu IsCollapsed=_isSidebarCollapsed />
|
||||||
|
<RadzenText class="rz-py-2 rz-mt-auto rz-text-contrast-color"
|
||||||
|
TextStyle=TextStyle.Subtitle2
|
||||||
|
TextAlign=TextAlign.Center
|
||||||
|
Visible=!_isSidebarCollapsed>
|
||||||
|
v.
|
||||||
|
@VersionProvider.Version
|
||||||
|
</RadzenText>
|
||||||
|
</RadzenSidebar>
|
||||||
|
<RadzenHeader>
|
||||||
|
<RadzenStack class="rz-mx-4"
|
||||||
|
Style="height: 100%;"
|
||||||
|
Orientation=Orientation.Horizontal
|
||||||
|
AlignItems=AlignItems.Center
|
||||||
|
Gap="0">
|
||||||
|
<RadzenBreadCrumb class="rz-me-auto">
|
||||||
|
<RadzenBreadCrumbItem Path="/" Text=@Strings["Home"] />
|
||||||
|
</RadzenBreadCrumb>
|
||||||
|
<UserMenu />
|
||||||
|
</RadzenStack>
|
||||||
|
</RadzenHeader>
|
||||||
|
<RadzenBody>
|
||||||
|
<AlertErrorBoundary>
|
||||||
|
@Body
|
||||||
|
</AlertErrorBoundary>
|
||||||
|
</RadzenBody>
|
||||||
|
</RadzenLayout>
|
||||||
|
|
||||||
|
<RadzenComponents />
|
||||||
124
src/Client/Layout/MainLayout.resx
Normal file
124
src/Client/Layout/MainLayout.resx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||||
|
<xsd:attribute name="type" type="xsd:string"/>
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||||
|
<xsd:attribute ref="xml:space"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string"/>
|
||||||
|
<xsd:attribute name="name" type="xsd:string"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||||
|
<xsd:attribute ref="xml:space"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="AppTitle" xml:space="preserve">
|
||||||
|
<value>SoaFE Portal</value>
|
||||||
|
<comment/>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
34
src/Client/Layout/NavMenu.cs
Normal file
34
src/Client/Layout/NavMenu.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Radzen;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Client.Layout;
|
||||||
|
|
||||||
|
public partial class NavMenu : IDisposable
|
||||||
|
{
|
||||||
|
#region Parameters
|
||||||
|
|
||||||
|
[Parameter] public bool IsCollapsed { get; set; }
|
||||||
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
[Inject] private IStringLocalizer<NavMenu> Strings { get; set; } = default!;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
private MenuItemDisplayStyle DisplayStyle
|
||||||
|
=> IsCollapsed
|
||||||
|
? MenuItemDisplayStyle.Icon
|
||||||
|
: MenuItemDisplayStyle.IconAndText;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
3
src/Client/Layout/NavMenu.razor
Normal file
3
src/Client/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<RadzenPanelMenu DisplayStyle=DisplayStyle
|
||||||
|
Multiple=false>
|
||||||
|
</RadzenPanelMenu>
|
||||||
59
src/Client/Layout/UserMenu.cs
Normal file
59
src/Client/Layout/UserMenu.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using SoaFE.Portal.Core;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Client.Layout;
|
||||||
|
|
||||||
|
public partial class UserMenu
|
||||||
|
{
|
||||||
|
#region Parameters
|
||||||
|
|
||||||
|
[CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!;
|
||||||
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
[Inject] private IStringLocalizer<UserMenu> Strings { get; set; } = default!;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
private AuthenticationState? _authenticationState;
|
||||||
|
private string? _avatarUrl;
|
||||||
|
private string? _userName;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
_authenticationState = await AuthenticationStateTask;
|
||||||
|
_avatarUrl = _authenticationState.User.FindFirst(Constants.DiscordAvatarClaim)?.Value;
|
||||||
|
_userName = _authenticationState.User.Identity?.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mutators
|
||||||
|
|
||||||
|
private void SignIn()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/auth/sign-in", forceLoad: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SignOut()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/auth/sign-out", forceLoad: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Callbacks
|
||||||
|
|
||||||
|
private void SignInOnClick()
|
||||||
|
=> SignIn();
|
||||||
|
private void SignOutOnClick()
|
||||||
|
=> SignOut();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
43
src/Client/Layout/UserMenu.razor
Normal file
43
src/Client/Layout/UserMenu.razor
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="user-menu">
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<RadzenProfileMenu>
|
||||||
|
<Template>
|
||||||
|
<RadzenStack Orientation=Orientation.Horizontal
|
||||||
|
AlignItems=AlignItems.End
|
||||||
|
Gap="0.5rem">
|
||||||
|
<RadzenImage class="avatar"
|
||||||
|
Path=@_avatarUrl />
|
||||||
|
<RadzenText Text=@_userName />
|
||||||
|
</RadzenStack>
|
||||||
|
</Template>
|
||||||
|
<ChildContent>
|
||||||
|
<RadzenProfileMenuItem Icon="settings"
|
||||||
|
Text=@Strings["Preferences"] />
|
||||||
|
<RadzenProfileMenuItem Icon="logout"
|
||||||
|
Text=@Strings["SignOut"]
|
||||||
|
Path="/auth/sign-out" />
|
||||||
|
</ChildContent>
|
||||||
|
</RadzenProfileMenu>
|
||||||
|
</Authorized>
|
||||||
|
|
||||||
|
<Authorizing>
|
||||||
|
<RadzenSkeleton class="avatar"
|
||||||
|
Variant=SkeletonVariant.Circular />
|
||||||
|
</Authorizing>
|
||||||
|
|
||||||
|
<NotAuthorized>
|
||||||
|
<RadzenButton Variant=Variant.Outlined
|
||||||
|
ButtonStyle=ButtonStyle.Light
|
||||||
|
Click=SignInOnClick>
|
||||||
|
<RadzenStack Orientation=Orientation.Horizontal
|
||||||
|
AlignItems=AlignItems.End
|
||||||
|
Gap="0.5rem">
|
||||||
|
<RadzenImage class="avatar"
|
||||||
|
Path="/images/discord.svg" />
|
||||||
|
<RadzenText Text=@Strings["SignIn"] />
|
||||||
|
</RadzenStack>
|
||||||
|
</RadzenButton>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
</div>
|
||||||
4
src/Client/Layout/UserMenu.razor.css
Normal file
4
src/Client/Layout/UserMenu.razor.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.user-menu ::deep .avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
24
src/Client/Layout/UserMenu.resx
Normal file
24
src/Client/Layout/UserMenu.resx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="Preferences" xml:space="preserve">
|
||||||
|
<value>Preferences</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignIn" xml:space="preserve">
|
||||||
|
<value>Sign In</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignOut" xml:space="preserve">
|
||||||
|
<value>Sign Out</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
1
src/Client/Layout/_Imports.razor
Normal file
1
src/Client/Layout/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Client.Layout
|
||||||
12
src/Client/Program.cs
Normal file
12
src/Client/Program.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using SoaFE.Portal.Core.Extensions;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
builder.Services.AddSoaFEPortalCore();
|
||||||
|
builder.Services.AddAuthorizationCore();
|
||||||
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
builder.Services.AddAuthenticationStateDeserialization();
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
6
src/Client/Routes.razor
Normal file
6
src/Client/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Features.Home.Pages.NotFound)">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
16
src/Client/_Imports.razor
Normal file
16
src/Client/_Imports.razor
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@namespace SoaFE.Portal.Client
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using Radzen
|
||||||
|
@using Radzen.Blazor
|
||||||
|
@using SoaFE.Portal.Client.Layout
|
||||||
|
@using SoaFE.Portal.Core.Components
|
||||||
|
@using SoaFE.Portal.Core.Services
|
||||||
8
src/Client/wwwroot/appsettings.Development.json
Normal file
8
src/Client/wwwroot/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Client/wwwroot/appsettings.json
Normal file
8
src/Client/wwwroot/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/Client/wwwroot/images/discord.svg
Normal file
3
src/Client/wwwroot/images/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36">
|
||||||
|
<path fill="#5865F2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 772 B |
22
src/Core/Components/AlertErrorBoundary.cs
Normal file
22
src/Core/Components/AlertErrorBoundary.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Radzen;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Core.Components;
|
||||||
|
|
||||||
|
public sealed class AlertErrorBoundary : ErrorBoundary
|
||||||
|
{
|
||||||
|
#region Parameters
|
||||||
|
|
||||||
|
[Inject] private DialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected override async Task OnErrorAsync(Exception exception)
|
||||||
|
{
|
||||||
|
await DialogService.Alert(
|
||||||
|
message: exception.Message,
|
||||||
|
title: "An error occurred"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/Core/Components/_Imports.razor
Normal file
1
src/Core/Components/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Core.Components
|
||||||
8
src/Core/Constants.cs
Normal file
8
src/Core/Constants.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Core;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string DiscordAvatarClaim = "urn:discord:avatar:url";
|
||||||
|
}
|
||||||
26
src/Core/Core.csproj
Normal file
26
src/Core/Core.csproj
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>$(Namespace).Core</RootNamespace>
|
||||||
|
<AssemblyName>$(Namespace).Core</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<SupportedPlatform Include="browser" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazilla" Version="*" />
|
||||||
|
<PackageReference Include="Blazored.LocalStorage" Version="*" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Localization" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Radzen.Blazor" Version="8.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Components/" />
|
||||||
|
<Folder Include="Services/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
19
src/Core/Extensions/DateTimeExtensions.cs
Normal file
19
src/Core/Extensions/DateTimeExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace SoaFE.Portal.Core.Extensions;
|
||||||
|
|
||||||
|
public static class DateTimeExtensions
|
||||||
|
{
|
||||||
|
public static DateTime ToLocalDateTime(this TimeProvider timeProvider, DateTime dateTime)
|
||||||
|
=> dateTime.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Local => dateTime,
|
||||||
|
DateTimeKind.Utc => DateTime.SpecifyKind(TimeZoneInfo.ConvertTimeFromUtc(dateTime, timeProvider.LocalTimeZone), DateTimeKind.Local),
|
||||||
|
_ => throw new InvalidOperationException("Unspecified Date Time Kind.")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static DateTime ToLocalDateTime(this TimeProvider timeProvider, DateTimeOffset dateTimeOffset)
|
||||||
|
{
|
||||||
|
var localDateTime = TimeZoneInfo.ConvertTime(dateTimeOffset, timeProvider.LocalTimeZone).DateTime;
|
||||||
|
|
||||||
|
return DateTime.SpecifyKind(localDateTime, DateTimeKind.Local);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Core/Extensions/ServiceCollectionExtensions.cs
Normal file
24
src/Core/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Blazored.LocalStorage;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Radzen;
|
||||||
|
using SoaFE.Portal.Core.Services;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Core.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddSoaFEPortalCore(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// Configure Radzen
|
||||||
|
services.AddRadzenComponents();
|
||||||
|
|
||||||
|
// Configure LocalStorage
|
||||||
|
services.AddBlazoredLocalStorage();
|
||||||
|
|
||||||
|
// Configure BrowserTimeProvider
|
||||||
|
services.AddScoped<BrowserTimeProvider>();
|
||||||
|
services.AddSingleton<VersionProvider>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/Core/Feature.cs
Normal file
3
src/Core/Feature.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace SoaFE.Portal.Core;
|
||||||
|
|
||||||
|
public abstract record Feature;
|
||||||
38
src/Core/Services/BrowserTimeProvider.cs
Normal file
38
src/Core/Services/BrowserTimeProvider.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Core.Services;
|
||||||
|
|
||||||
|
public sealed class BrowserTimeProvider : TimeProvider
|
||||||
|
{
|
||||||
|
private TimeZoneInfo? _browserTime;
|
||||||
|
|
||||||
|
[MemberNotNullWhen(true, nameof(_browserTime))]
|
||||||
|
internal bool HasBrowserTime =>
|
||||||
|
_browserTime is not null;
|
||||||
|
|
||||||
|
public event EventHandler? BrowserTimeZomeChanged;
|
||||||
|
|
||||||
|
public override TimeZoneInfo LocalTimeZone =>
|
||||||
|
_browserTime ?? base.LocalTimeZone;
|
||||||
|
|
||||||
|
public void SetBrowserTimeZone(string timeZoneIanaId)
|
||||||
|
{
|
||||||
|
var hasIanaTimeZone = TimeZoneInfo.TryFindSystemTimeZoneById(timeZoneIanaId, out var tzInfo);
|
||||||
|
|
||||||
|
if (!hasIanaTimeZone)
|
||||||
|
{
|
||||||
|
var hasWinTimeZone = TimeZoneInfo.TryConvertIanaIdToWindowsId(timeZoneIanaId, out var timeZoneWinId);
|
||||||
|
|
||||||
|
if (hasWinTimeZone)
|
||||||
|
{
|
||||||
|
TimeZoneInfo.TryFindSystemTimeZoneById(timeZoneWinId!, out tzInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tzInfo is not null && tzInfo != _browserTime)
|
||||||
|
{
|
||||||
|
_browserTime = tzInfo;
|
||||||
|
BrowserTimeZomeChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Core/Services/VersionProvider.cs
Normal file
11
src/Core/Services/VersionProvider.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Core.Services;
|
||||||
|
|
||||||
|
public sealed class VersionProvider
|
||||||
|
{
|
||||||
|
public string Version { get; } = Assembly.GetExecutingAssembly()!
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion
|
||||||
|
.Split('+')
|
||||||
|
.First();
|
||||||
|
}
|
||||||
6
src/Core/_Imports.razor
Normal file
6
src/Core/_Imports.razor
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@namespace SoaFE.Portal.Core
|
||||||
|
|
||||||
|
@using Blazored.LocalStorage
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Radzen
|
||||||
|
@using Radzen.Blazor
|
||||||
7
src/Core/wwwroot/SoaFE.Portal.Core.js
Normal file
7
src/Core/wwwroot/SoaFE.Portal.Core.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
window.SoaFE = {
|
||||||
|
getBrowserTimeZone: function () {
|
||||||
|
const options = Intl.DateTimeFormat().resolvedOptions();
|
||||||
|
|
||||||
|
return options.timeZone;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Data/Data.csproj
Normal file
22
src/Data/Data.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>$(Namespace).Data</RootNamespace>
|
||||||
|
<AssemblyName>$(Namespace).Data</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(DotNetVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Extensions/" />
|
||||||
|
<Folder Include="Internal/" />
|
||||||
|
<Folder Include="Models/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Extensions;
|
||||||
|
|
||||||
|
public static class NpgsqlDbContextOptionsBuilderExtensions
|
||||||
|
{
|
||||||
|
public static NpgsqlDbContextOptionsBuilder MapSoaFEPortalEnums(this NpgsqlDbContextOptionsBuilder builder)
|
||||||
|
{
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/Data/Functions.cs
Normal file
6
src/Data/Functions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace SoaFE.Portal.Data;
|
||||||
|
|
||||||
|
public static class Functions
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
34
src/Data/Internal/SoaFEPortalHistoryRepository.cs
Normal file
34
src/Data/Internal/SoaFEPortalHistoryRepository.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Internal;
|
||||||
|
|
||||||
|
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||||
|
internal class SoaFEPortalHistoryRepository : NpgsqlHistoryRepository
|
||||||
|
{
|
||||||
|
public SoaFEPortalHistoryRepository(HistoryRepositoryDependencies dependencies) : base(dependencies)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureTable(EntityTypeBuilder<HistoryRow> history)
|
||||||
|
{
|
||||||
|
base.ConfigureTable(history);
|
||||||
|
history.HasKey(e => e.MigrationId)
|
||||||
|
.HasName("pk_migrations_history");
|
||||||
|
|
||||||
|
var order = 0;
|
||||||
|
history.Property(e => e.MigrationId)
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnName("migration_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
history.Property(e => e.ProductVersion)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnName("product_version")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore EF1001 // Internal EF Core API usage.
|
||||||
516
src/Data/Migrations/20260221191123_AddIdentity.Designer.cs
generated
Normal file
516
src/Data/Migrations/20260221191123_AddIdentity.Designer.cs
generated
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SoaFE.Portal.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SoaFEDbContext))]
|
||||||
|
[Migration("20260221191123_AddIdentity")]
|
||||||
|
partial class AddIdentity
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("AppSettingKey")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(16)")
|
||||||
|
.HasColumnName("app_setting_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||||
|
.HasPrecision(0)
|
||||||
|
.HasColumnType("timestamp(0) with time zone")
|
||||||
|
.HasColumnName("modified_at")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<long?>("ModifiedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("modified_by")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<JsonElement?>("Value")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("AppSettingKey")
|
||||||
|
.HasName("pk_app_setting");
|
||||||
|
|
||||||
|
b.HasIndex("ModifiedBy")
|
||||||
|
.HasDatabaseName("ix_app_setting_modified_by");
|
||||||
|
|
||||||
|
b.ToTable("app_setting", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("normalized_name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_role");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("uix_role_normalized_name");
|
||||||
|
|
||||||
|
b.ToTable("role", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.RoleClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role_claim_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<long>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_role_claim");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId")
|
||||||
|
.HasDatabaseName("ix_role_claim_role_key");
|
||||||
|
|
||||||
|
b.ToTable("role_claim", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("avatar_url")
|
||||||
|
.HasColumnOrder(5);
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(4);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastLogin")
|
||||||
|
.HasPrecision(0)
|
||||||
|
.HasColumnType("timestamp(0) with time zone")
|
||||||
|
.HasColumnName("last_login")
|
||||||
|
.HasColumnOrder(6);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("normalized_username")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("security_stamp")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("username")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_user");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("uix_user_normalized_username");
|
||||||
|
|
||||||
|
b.ToTable("user", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_username_minimum_length", "length(username) >= 2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_claim_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_user_claim");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_claim_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_claim", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("provider_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("provider_display_name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey")
|
||||||
|
.HasName("pk_user_login");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_login_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_login", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserPasskey", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<byte[]>("CredentialId")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("bytea")
|
||||||
|
.HasColumnName("credential_id")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "CredentialId")
|
||||||
|
.HasName("pk_user_passkey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_passkey_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_passkey", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<long>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId")
|
||||||
|
.HasName("pk_user_role");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId")
|
||||||
|
.HasDatabaseName("ix_user_role_role_key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_role_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_role", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name")
|
||||||
|
.HasName("pk_user_token");
|
||||||
|
|
||||||
|
b.ToTable("user_token", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "ModifiedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ModifiedBy")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull)
|
||||||
|
.HasConstraintName("fk_app_setting_user_modified_by");
|
||||||
|
|
||||||
|
b.Navigation("ModifiedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.RoleClaim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.Role", "Role")
|
||||||
|
.WithMany("Claims")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_role_role_claim");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserClaim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Claims")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_claim");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Logins")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_login");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserPasskey", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Passkeys")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_passkey");
|
||||||
|
|
||||||
|
b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("UserPasskeyUserId");
|
||||||
|
|
||||||
|
b1.Property<byte[]>("UserPasskeyCredentialId");
|
||||||
|
|
||||||
|
b1.Property<byte[]>("AttestationObject")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<byte[]>("ClientDataJson")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasPrecision(0);
|
||||||
|
|
||||||
|
b1.Property<bool>("IsBackedUp");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsBackupEligible");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsUserVerified");
|
||||||
|
|
||||||
|
b1.Property<string>("Name")
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
b1.Property<byte[]>("PublicKey")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<long>("SignCount");
|
||||||
|
|
||||||
|
b1.PrimitiveCollection<string>("Transports");
|
||||||
|
|
||||||
|
b1.HasKey("UserPasskeyUserId", "UserPasskeyCredentialId");
|
||||||
|
|
||||||
|
b1.ToTable("user_passkey");
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("data")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("UserPasskeyUserId", "UserPasskeyCredentialId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Data")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.Role", "Role")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_role_user_role");
|
||||||
|
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_role");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Tokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_token");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Claims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Claims");
|
||||||
|
|
||||||
|
b.Navigation("Logins");
|
||||||
|
|
||||||
|
b.Navigation("Passkeys");
|
||||||
|
|
||||||
|
b.Navigation("Tokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
273
src/Data/Migrations/20260221191123_AddIdentity.cs
Normal file
273
src/Data/Migrations/20260221191123_AddIdentity.cs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIdentity : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "role",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
role_key = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
normalized_name = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
concurrency_stamp = table.Column<string>(type: "text", unicode: false, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_role", x => x.role_key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
username = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
normalized_username = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
security_stamp = table.Column<string>(type: "text", nullable: true),
|
||||||
|
concurrency_stamp = table.Column<string>(type: "text", unicode: false, nullable: true),
|
||||||
|
avatar_url = table.Column<string>(type: "character varying(4096)", unicode: false, maxLength: 4096, nullable: true),
|
||||||
|
last_login = table.Column<DateTimeOffset>(type: "timestamp(0) with time zone", precision: 0, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user", x => x.user_key);
|
||||||
|
table.CheckConstraint("ck_username_minimum_length", "length(username) >= 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "role_claim",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
role_claim_key = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
role_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
claim_type = table.Column<string>(type: "character varying(256)", unicode: false, maxLength: 256, nullable: false),
|
||||||
|
claim_value = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_role_claim", x => x.role_claim_key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_role_role_claim",
|
||||||
|
column: x => x.role_key,
|
||||||
|
principalTable: "role",
|
||||||
|
principalColumn: "role_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "app_setting",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
app_setting_key = table.Column<string>(type: "character varying(16)", unicode: false, maxLength: 16, nullable: false),
|
||||||
|
value = table.Column<JsonElement>(type: "jsonb", nullable: true),
|
||||||
|
modified_at = table.Column<DateTimeOffset>(type: "timestamp(0) with time zone", precision: 0, nullable: true),
|
||||||
|
modified_by = table.Column<long>(type: "bigint", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_app_setting", x => x.app_setting_key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_app_setting_user_modified_by",
|
||||||
|
column: x => x.modified_by,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_claim",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_claim_key = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
claim_type = table.Column<string>(type: "character varying(256)", unicode: false, maxLength: 256, nullable: false),
|
||||||
|
claim_value = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user_claim", x => x.user_claim_key);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_user_user_claim",
|
||||||
|
column: x => x.user_key,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_login",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
login_provider = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
provider_key = table.Column<string>(type: "text", nullable: false),
|
||||||
|
provider_display_name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user_login", x => new { x.login_provider, x.provider_key });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_user_user_login",
|
||||||
|
column: x => x.user_key,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_passkey",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
credential_id = table.Column<byte[]>(type: "bytea", maxLength: 256, nullable: false),
|
||||||
|
data = table.Column<string>(type: "jsonb", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user_passkey", x => new { x.user_key, x.credential_id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_user_user_passkey",
|
||||||
|
column: x => x.user_key,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_role",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
role_key = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user_role", x => new { x.user_key, x.role_key });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_role_user_role",
|
||||||
|
column: x => x.role_key,
|
||||||
|
principalTable: "role",
|
||||||
|
principalColumn: "role_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_user_user_role",
|
||||||
|
column: x => x.user_key,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user_token",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
user_key = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
login_provider = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(32)", unicode: false, maxLength: 32, nullable: false),
|
||||||
|
value = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_user_token", x => new { x.user_key, x.login_provider, x.name });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_user_user_token",
|
||||||
|
column: x => x.user_key,
|
||||||
|
principalTable: "user",
|
||||||
|
principalColumn: "user_key",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_app_setting_modified_by",
|
||||||
|
table: "app_setting",
|
||||||
|
column: "modified_by");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "uix_role_normalized_name",
|
||||||
|
table: "role",
|
||||||
|
column: "normalized_name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_role_claim_role_key",
|
||||||
|
table: "role_claim",
|
||||||
|
column: "role_key");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "uix_user_normalized_username",
|
||||||
|
table: "user",
|
||||||
|
column: "normalized_username",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_user_claim_user_key",
|
||||||
|
table: "user_claim",
|
||||||
|
column: "user_key");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_user_login_user_key",
|
||||||
|
table: "user_login",
|
||||||
|
column: "user_key");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_user_passkey_user_key",
|
||||||
|
table: "user_passkey",
|
||||||
|
column: "user_key");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_user_role_role_key",
|
||||||
|
table: "user_role",
|
||||||
|
column: "role_key");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_user_role_user_key",
|
||||||
|
table: "user_role",
|
||||||
|
column: "user_key");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "app_setting");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "role_claim");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_claim");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_login");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_passkey");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_role");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user_token");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "role");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/Data/Migrations/AddIdentity.sql
Normal file
111
src/Data/Migrations/AddIdentity.sql
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS _migrations_history (
|
||||||
|
migration_key character varying(64) NOT NULL,
|
||||||
|
product_version character varying(32) NOT NULL,
|
||||||
|
CONSTRAINT pk_migrations_history PRIMARY KEY (migration_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
CREATE TABLE role (
|
||||||
|
role_key bigint GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
name character varying(32) NOT NULL,
|
||||||
|
normalized_name character varying(32) NOT NULL,
|
||||||
|
concurrency_stamp text,
|
||||||
|
CONSTRAINT pk_role PRIMARY KEY (role_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
username character varying(32) NOT NULL,
|
||||||
|
normalized_username character varying(32) NOT NULL,
|
||||||
|
security_stamp text,
|
||||||
|
concurrency_stamp text,
|
||||||
|
avatar_url character varying(4096),
|
||||||
|
last_login timestamp(0) with time zone,
|
||||||
|
CONSTRAINT pk_user PRIMARY KEY (user_key),
|
||||||
|
CONSTRAINT ck_username_minimum_length CHECK (length(username) >= 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE role_claim (
|
||||||
|
role_claim_key integer GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
role_key bigint NOT NULL,
|
||||||
|
claim_type character varying(256) NOT NULL,
|
||||||
|
claim_value text,
|
||||||
|
CONSTRAINT pk_role_claim PRIMARY KEY (role_claim_key),
|
||||||
|
CONSTRAINT fk_role_role_claim FOREIGN KEY (role_key) REFERENCES role (role_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE app_setting (
|
||||||
|
app_setting_key character varying(16) NOT NULL,
|
||||||
|
value jsonb,
|
||||||
|
modified_at timestamp(0) with time zone,
|
||||||
|
modified_by bigint,
|
||||||
|
CONSTRAINT pk_app_setting PRIMARY KEY (app_setting_key),
|
||||||
|
CONSTRAINT fk_app_setting_user_modified_by FOREIGN KEY (modified_by) REFERENCES "user" (user_key) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_claim (
|
||||||
|
user_claim_key integer GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
claim_type character varying(256) NOT NULL,
|
||||||
|
claim_value text,
|
||||||
|
CONSTRAINT pk_user_claim PRIMARY KEY (user_claim_key),
|
||||||
|
CONSTRAINT fk_user_user_claim FOREIGN KEY (user_key) REFERENCES "user" (user_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_login (
|
||||||
|
login_provider character varying(32) NOT NULL,
|
||||||
|
provider_key text NOT NULL,
|
||||||
|
provider_display_name character varying(32),
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
CONSTRAINT pk_user_login PRIMARY KEY (login_provider, provider_key),
|
||||||
|
CONSTRAINT fk_user_user_login FOREIGN KEY (user_key) REFERENCES "user" (user_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_passkey (
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
credential_id bytea NOT NULL,
|
||||||
|
data jsonb NOT NULL,
|
||||||
|
CONSTRAINT pk_user_passkey PRIMARY KEY (user_key, credential_id),
|
||||||
|
CONSTRAINT fk_user_user_passkey FOREIGN KEY (user_key) REFERENCES "user" (user_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_role (
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
role_key bigint NOT NULL,
|
||||||
|
CONSTRAINT pk_user_role PRIMARY KEY (user_key, role_key),
|
||||||
|
CONSTRAINT fk_role_user_role FOREIGN KEY (role_key) REFERENCES role (role_key) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_user_user_role FOREIGN KEY (user_key) REFERENCES "user" (user_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_token (
|
||||||
|
user_key bigint NOT NULL,
|
||||||
|
login_provider character varying(32) NOT NULL,
|
||||||
|
name character varying(32) NOT NULL,
|
||||||
|
value text,
|
||||||
|
CONSTRAINT pk_user_token PRIMARY KEY (user_key, login_provider, name),
|
||||||
|
CONSTRAINT fk_user_user_token FOREIGN KEY (user_key) REFERENCES "user" (user_key) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX ix_app_setting_modified_by ON app_setting (modified_by);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uix_role_normalized_name ON role (normalized_name);
|
||||||
|
|
||||||
|
CREATE INDEX ix_role_claim_role_key ON role_claim (role_key);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uix_user_normalized_username ON "user" (normalized_username);
|
||||||
|
|
||||||
|
CREATE INDEX ix_user_claim_user_key ON user_claim (user_key);
|
||||||
|
|
||||||
|
CREATE INDEX ix_user_login_user_key ON user_login (user_key);
|
||||||
|
|
||||||
|
CREATE INDEX ix_user_passkey_user_key ON user_passkey (user_key);
|
||||||
|
|
||||||
|
CREATE INDEX ix_user_role_role_key ON user_role (role_key);
|
||||||
|
|
||||||
|
CREATE INDEX ix_user_role_user_key ON user_role (user_key);
|
||||||
|
|
||||||
|
INSERT INTO _migrations_history (migration_key, product_version)
|
||||||
|
VALUES ('20260221191123_AddIdentity', '10.0.3');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
513
src/Data/Migrations/SoaFEDbContextModelSnapshot.cs
Normal file
513
src/Data/Migrations/SoaFEDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using SoaFE.Portal.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SoaFEDbContext))]
|
||||||
|
partial class SoaFEDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("AppSettingKey")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(16)")
|
||||||
|
.HasColumnName("app_setting_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||||
|
.HasPrecision(0)
|
||||||
|
.HasColumnType("timestamp(0) with time zone")
|
||||||
|
.HasColumnName("modified_at")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<long?>("ModifiedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("modified_by")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<JsonElement?>("Value")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("AppSettingKey")
|
||||||
|
.HasName("pk_app_setting");
|
||||||
|
|
||||||
|
b.HasIndex("ModifiedBy")
|
||||||
|
.HasDatabaseName("ix_app_setting_modified_by");
|
||||||
|
|
||||||
|
b.ToTable("app_setting", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("normalized_name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_role");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("uix_role_normalized_name");
|
||||||
|
|
||||||
|
b.ToTable("role", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.RoleClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role_claim_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<long>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_role_claim");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId")
|
||||||
|
.HasDatabaseName("ix_role_claim_role_key");
|
||||||
|
|
||||||
|
b.ToTable("role_claim", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("avatar_url")
|
||||||
|
.HasColumnOrder(5);
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(4);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastLogin")
|
||||||
|
.HasPrecision(0)
|
||||||
|
.HasColumnType("timestamp(0) with time zone")
|
||||||
|
.HasColumnName("last_login")
|
||||||
|
.HasColumnOrder(6);
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("normalized_username")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("security_stamp")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("username")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_user");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("uix_user_normalized_username");
|
||||||
|
|
||||||
|
b.ToTable("user", null, t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_username_minimum_length", "length(username) >= 2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_claim_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_user_claim");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_claim_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_claim", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("provider_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("provider_display_name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey")
|
||||||
|
.HasName("pk_user_login");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_login_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_login", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserPasskey", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<byte[]>("CredentialId")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("bytea")
|
||||||
|
.HasColumnName("credential_id")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "CredentialId")
|
||||||
|
.HasName("pk_user_passkey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_passkey_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_passkey", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<long>("RoleId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId")
|
||||||
|
.HasName("pk_user_role");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId")
|
||||||
|
.HasDatabaseName("ix_user_role_role_key");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_user_role_user_key");
|
||||||
|
|
||||||
|
b.ToTable("user_role", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("UserId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(0);
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(1);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(2);
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsUnicode(true)
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnOrder(3);
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name")
|
||||||
|
.HasName("pk_user_token");
|
||||||
|
|
||||||
|
b.ToTable("user_token", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "ModifiedByUser")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ModifiedBy")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull)
|
||||||
|
.HasConstraintName("fk_app_setting_user_modified_by");
|
||||||
|
|
||||||
|
b.Navigation("ModifiedByUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.RoleClaim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.Role", "Role")
|
||||||
|
.WithMany("Claims")
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_role_role_claim");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserClaim", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Claims")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_claim");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserLogin", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Logins")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_login");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserPasskey", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Passkeys")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_passkey");
|
||||||
|
|
||||||
|
b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<long>("UserPasskeyUserId");
|
||||||
|
|
||||||
|
b1.Property<byte[]>("UserPasskeyCredentialId");
|
||||||
|
|
||||||
|
b1.Property<byte[]>("AttestationObject")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<byte[]>("ClientDataJson")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasPrecision(0);
|
||||||
|
|
||||||
|
b1.Property<bool>("IsBackedUp");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsBackupEligible");
|
||||||
|
|
||||||
|
b1.Property<bool>("IsUserVerified");
|
||||||
|
|
||||||
|
b1.Property<string>("Name")
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
b1.Property<byte[]>("PublicKey")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.Property<long>("SignCount");
|
||||||
|
|
||||||
|
b1.PrimitiveCollection<string>("Transports");
|
||||||
|
|
||||||
|
b1.HasKey("UserPasskeyUserId", "UserPasskeyCredentialId");
|
||||||
|
|
||||||
|
b1.ToTable("user_passkey");
|
||||||
|
|
||||||
|
b1
|
||||||
|
.ToJson("data")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("UserPasskeyUserId", "UserPasskeyCredentialId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Data")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.Role", "Role")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_role_user_role");
|
||||||
|
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_role");
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.UserToken", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SoaFE.Portal.Data.Models.User", "User")
|
||||||
|
.WithMany("Tokens")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_user_user_token");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.Role", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Claims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SoaFE.Portal.Data.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Claims");
|
||||||
|
|
||||||
|
b.Navigation("Logins");
|
||||||
|
|
||||||
|
b.Navigation("Passkeys");
|
||||||
|
|
||||||
|
b.Navigation("Tokens");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/Data/Models/AppSetting.cs
Normal file
57
src/Data/Models/AppSetting.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class AppSetting : IEntityTypeConfiguration<AppSetting>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
public required string AppSettingKey { get; set; }
|
||||||
|
public JsonElement? Value { get; set; }
|
||||||
|
public DateTimeOffset? ModifiedAt { get; set; }
|
||||||
|
public long? ModifiedBy { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User? ModifiedByUser { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<AppSetting> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("app_setting");
|
||||||
|
builder.HasKey(e => e.AppSettingKey)
|
||||||
|
.HasName("pk_app_setting");
|
||||||
|
builder.HasIndex(e => e.ModifiedBy)
|
||||||
|
.HasDatabaseName("ix_app_setting_modified_by");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.AppSettingKey)
|
||||||
|
.HasColumnName("app_setting_key")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.Value)
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ModifiedAt)
|
||||||
|
.HasColumnName("modified_at")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ModifiedBy)
|
||||||
|
.HasColumnName("modified_by")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
|
||||||
|
// Configure Relationships
|
||||||
|
builder.HasOne(e => e.ModifiedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.ModifiedBy)
|
||||||
|
.HasConstraintName("fk_app_setting_user_modified_by")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/Data/Models/Role.cs
Normal file
65
src/Data/Models/Role.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class Role : IdentityRole<long>, IEntityTypeConfiguration<Role>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
public override required string? Name { get; set; }
|
||||||
|
public override required string? NormalizedName { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public ICollection<RoleClaim> Claims { get; } = [];
|
||||||
|
public ICollection<User> Users { get; } = [];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<Role> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("role");
|
||||||
|
builder.HasKey(e => e.Id)
|
||||||
|
.HasName("pk_role");
|
||||||
|
builder.HasIndex(e => e.NormalizedName)
|
||||||
|
.HasDatabaseName("uix_role_normalized_name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.Id)
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.Name)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.NormalizedName)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("normalized_name")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ConcurrencyStamp)
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
|
||||||
|
// Configure Relationships
|
||||||
|
builder.HasMany(e => e.Claims)
|
||||||
|
.WithOne(e => e.Role)
|
||||||
|
.HasForeignKey(e => e.RoleId)
|
||||||
|
.HasConstraintName("fk_role_role_claim")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
builder.HasMany(e => e.Users)
|
||||||
|
.WithMany(e => e.Roles)
|
||||||
|
.UsingEntity<UserRole>(
|
||||||
|
UserRole.ConfigureRight,
|
||||||
|
UserRole.ConfigureLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Data/Models/RoleClaim.cs
Normal file
52
src/Data/Models/RoleClaim.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class RoleClaim : IdentityRoleClaim<long>, IEntityTypeConfiguration<RoleClaim>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(Role))]
|
||||||
|
public override required long RoleId { get; set; }
|
||||||
|
public override required string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public Role Role { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<RoleClaim> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("role_claim");
|
||||||
|
builder.HasKey(e => e.Id)
|
||||||
|
.HasName("pk_role_claim");
|
||||||
|
builder.HasIndex(e => e.RoleId)
|
||||||
|
.HasDatabaseName("ix_role_claim_role_key");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.Id)
|
||||||
|
.HasColumnName("role_claim_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.RoleId)
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ClaimType)
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ClaimValue)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/Data/Models/User.cs
Normal file
112
src/Data/Models/User.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class User : IdentityUser<long>, IEntityTypeConfiguration<User>
|
||||||
|
{
|
||||||
|
public const int UserNameMinimumLength = 2;
|
||||||
|
public const int UserNameMaximumLength = 32;
|
||||||
|
public const int AvatarUrlMaximumLength = 4096;
|
||||||
|
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
public override required string? UserName { get; set; }
|
||||||
|
public override required string? NormalizedUserName { get; set; }
|
||||||
|
public string? AvatarUrl { get; set; }
|
||||||
|
public DateTimeOffset? LastLogin { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public ICollection<Role> Roles { get; } = [];
|
||||||
|
public ICollection<UserClaim> Claims { get; } = [];
|
||||||
|
public ICollection<UserLogin> Logins { get; } = [];
|
||||||
|
public ICollection<UserToken> Tokens { get; } = [];
|
||||||
|
public ICollection<UserPasskey> Passkeys { get; } = [];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<User> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasCheckConstraint("ck_username_minimum_length", $"length(username) >= {UserNameMinimumLength}");
|
||||||
|
});
|
||||||
|
builder.HasKey(e => e.Id)
|
||||||
|
.HasName("pk_user");
|
||||||
|
builder.HasIndex(e => e.NormalizedUserName)
|
||||||
|
.HasDatabaseName("uix_user_normalized_username")
|
||||||
|
.IsUnique();
|
||||||
|
builder.HasIndex(e => e.NormalizedEmail)
|
||||||
|
.HasDatabaseName("ix_user_normalized_email");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.Id)
|
||||||
|
.ValueGeneratedNever()
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.UserName)
|
||||||
|
.HasMaxLength(UserNameMaximumLength)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("username")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.NormalizedUserName)
|
||||||
|
.HasMaxLength(UserNameMaximumLength)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("normalized_username")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Ignore(e => e.Email);
|
||||||
|
builder.Ignore(e => e.NormalizedEmail);
|
||||||
|
builder.Ignore(e => e.EmailConfirmed);
|
||||||
|
builder.Ignore(e => e.PasswordHash);
|
||||||
|
builder.Property(e => e.SecurityStamp)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("security_stamp")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ConcurrencyStamp)
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnName("concurrency_stamp")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Ignore(e => e.PhoneNumber);
|
||||||
|
builder.Ignore(e => e.PhoneNumberConfirmed);
|
||||||
|
builder.Ignore(e => e.TwoFactorEnabled);
|
||||||
|
builder.Ignore(e => e.LockoutEnd);
|
||||||
|
builder.Ignore(e => e.LockoutEnabled);
|
||||||
|
builder.Ignore(e => e.AccessFailedCount);
|
||||||
|
builder.Property(e => e.AvatarUrl)
|
||||||
|
.HasMaxLength(AvatarUrlMaximumLength)
|
||||||
|
.HasColumnName("avatar_url")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.LastLogin)
|
||||||
|
.HasColumnName("last_login")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
|
||||||
|
// Configure Relationships
|
||||||
|
builder.HasMany(e => e.Claims)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.HasConstraintName("fk_user_user_claim")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
builder.HasMany(e => e.Logins)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.HasConstraintName("fk_user_user_login")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
builder.HasMany(e => e.Tokens)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.HasConstraintName("fk_user_user_token")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
builder.HasMany(e => e.Passkeys)
|
||||||
|
.WithOne(e => e.User)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.HasConstraintName("fk_user_user_passkey")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Data/Models/UserClaim.cs
Normal file
52
src/Data/Models/UserClaim.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class UserClaim : IdentityUserClaim<long>, IEntityTypeConfiguration<UserClaim>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(User))]
|
||||||
|
public override required long UserId { get; set; }
|
||||||
|
public override required string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User User { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<UserClaim> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user_claim");
|
||||||
|
builder.HasKey(e => e.Id)
|
||||||
|
.HasName("pk_user_claim");
|
||||||
|
builder.HasIndex(e => e.UserId)
|
||||||
|
.HasDatabaseName("ix_user_claim_user_key");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.Id)
|
||||||
|
.HasColumnName("user_claim_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ClaimType)
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnName("claim_type")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ClaimValue)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("claim_value")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Data/Models/UserLogin.cs
Normal file
58
src/Data/Models/UserLogin.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class UserLogin : IdentityUserLogin<long>, IEntityTypeConfiguration<UserLogin>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
public override required string LoginProvider { get; set; }
|
||||||
|
public override required string ProviderKey { get; set; }
|
||||||
|
[MemberNotNull(nameof(User))]
|
||||||
|
public override required long UserId { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User User { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<UserLogin> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user_login");
|
||||||
|
builder.HasKey(e => new
|
||||||
|
{
|
||||||
|
e.LoginProvider,
|
||||||
|
e.ProviderKey
|
||||||
|
})
|
||||||
|
.HasName("pk_user_login");
|
||||||
|
builder.HasIndex(e => e.UserId)
|
||||||
|
.HasDatabaseName("ix_user_login_user_key");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.LoginProvider)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ProviderKey)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("provider_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.ProviderDisplayName)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("provider_display_name")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/Data/Models/UserPasskey.cs
Normal file
56
src/Data/Models/UserPasskey.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class UserPasskey : IdentityUserPasskey<long>, IEntityTypeConfiguration<UserPasskey>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(User))]
|
||||||
|
public override required long UserId { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User User { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<UserPasskey> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user_passkey");
|
||||||
|
builder.HasKey(e => new
|
||||||
|
{
|
||||||
|
e.UserId,
|
||||||
|
e.CredentialId
|
||||||
|
})
|
||||||
|
.HasName("pk_user_passkey");
|
||||||
|
builder.HasIndex(e => e.UserId)
|
||||||
|
.HasDatabaseName("ix_user_passkey_user_key");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.CredentialId)
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("credential_id")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
|
||||||
|
// Configure Relationships
|
||||||
|
builder.OwnsOne(e => e.Data, Configure);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Configure(OwnedNavigationBuilder<UserPasskey, IdentityPasskeyData> builder)
|
||||||
|
{
|
||||||
|
builder.ToJson("data");
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Data/Models/UserRole.cs
Normal file
63
src/Data/Models/UserRole.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class UserRole : IdentityUserRole<long>, IEntityTypeConfiguration<UserRole>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(User))]
|
||||||
|
public override required long UserId { get; set; }
|
||||||
|
[MemberNotNull(nameof(Role))]
|
||||||
|
public override required long RoleId { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User User { get; internal set; }
|
||||||
|
public Role Role { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<UserRole> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user_role");
|
||||||
|
builder.HasKey(e => new
|
||||||
|
{
|
||||||
|
e.UserId,
|
||||||
|
e.RoleId
|
||||||
|
})
|
||||||
|
.HasName("pk_user_role");
|
||||||
|
builder.HasIndex(e => e.UserId)
|
||||||
|
.HasDatabaseName("ix_user_role_user_key");
|
||||||
|
builder.HasIndex(e => e.RoleId)
|
||||||
|
.HasDatabaseName("ix_user_role_role_key");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.RoleId)
|
||||||
|
.HasColumnName("role_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ReferenceCollectionBuilder<Role, UserRole> ConfigureLeft(EntityTypeBuilder<UserRole> builder)
|
||||||
|
=> builder.HasOne(e => e.Role)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.RoleId)
|
||||||
|
.HasConstraintName("fk_role_user_role");
|
||||||
|
|
||||||
|
internal static ReferenceCollectionBuilder<User, UserRole> ConfigureRight(EntityTypeBuilder<UserRole> builder)
|
||||||
|
=> builder.HasOne(e => e.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.HasConstraintName("fk_user_user_role");
|
||||||
|
}
|
||||||
56
src/Data/Models/UserToken.cs
Normal file
56
src/Data/Models/UserToken.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
public sealed class UserToken : IdentityUserToken<long>, IEntityTypeConfiguration<UserToken>
|
||||||
|
{
|
||||||
|
#region Scalar Properties
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(User))]
|
||||||
|
public override required long UserId { get; set; }
|
||||||
|
public override required string LoginProvider { get; set; }
|
||||||
|
public override required string Name { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Navigation Properties
|
||||||
|
|
||||||
|
public User User { get; internal set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<UserToken> builder)
|
||||||
|
{
|
||||||
|
// Configure Table
|
||||||
|
builder.ToTable("user_token");
|
||||||
|
builder.HasKey(e => new
|
||||||
|
{
|
||||||
|
e.UserId,
|
||||||
|
e.LoginProvider,
|
||||||
|
e.Name
|
||||||
|
})
|
||||||
|
.HasName("pk_user_token");
|
||||||
|
|
||||||
|
// Configure Columns
|
||||||
|
var order = 0;
|
||||||
|
builder.Property(e => e.UserId)
|
||||||
|
.HasColumnName("user_key")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.LoginProvider)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnName("login_provider")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.Name)
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
builder.Property(e => e.Value)
|
||||||
|
.IsUnicode()
|
||||||
|
.HasColumnName("value")
|
||||||
|
.HasColumnOrder(order++);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/Data/SoaFEDbContext.cs
Normal file
36
src/Data/SoaFEDbContext.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using SoaFE.Portal.Data.Internal;
|
||||||
|
using SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data;
|
||||||
|
|
||||||
|
public sealed class SoaFEDbContext : IdentityDbContext<User, Role, long, UserClaim, UserRole, UserLogin, RoleClaim, UserToken, UserPasskey>
|
||||||
|
{
|
||||||
|
public SoaFEDbContext(DbContextOptions<SoaFEDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTrackingWithIdentityResolution;
|
||||||
|
}
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.ReplaceService<IHistoryRepository, SoaFEPortalHistoryRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||||
|
{
|
||||||
|
configurationBuilder.Properties<string>()
|
||||||
|
.AreUnicode(false);
|
||||||
|
configurationBuilder.Properties<decimal>()
|
||||||
|
.HavePrecision(15, 6);
|
||||||
|
configurationBuilder.Properties<DateTime>()
|
||||||
|
.HavePrecision(0);
|
||||||
|
configurationBuilder.Properties<DateTimeOffset>()
|
||||||
|
.HavePrecision(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
builder.ApplyConfigurationsFromAssembly(typeof(SoaFEDbContext).Assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Data/SoaFEPortalDbDesignTimeFactory.cs
Normal file
33
src/Data/SoaFEPortalDbDesignTimeFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SoaFE.Portal.Data.Extensions;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Data;
|
||||||
|
|
||||||
|
internal class SoaFEPortalDbDesignTimeFactory : IDesignTimeDbContextFactory<SoaFEDbContext>
|
||||||
|
{
|
||||||
|
public SoaFEDbContext CreateDbContext(params string[] args)
|
||||||
|
{
|
||||||
|
var parentPath = Directory.GetParent(Directory.GetCurrentDirectory())!.FullName;
|
||||||
|
var basePath = Path.Combine(parentPath, "Server");
|
||||||
|
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(basePath)
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.AddJsonFile($"appsettings.{environment}.json", optional: true)
|
||||||
|
.AddUserSecrets("dffd14fa-856a-4713-8507-639dfd10dec0")
|
||||||
|
.Build();
|
||||||
|
var connectionString = configuration.GetConnectionString("SoaFEPortalDb");
|
||||||
|
var dbBuilder = new DbContextOptionsBuilder<SoaFEDbContext>();
|
||||||
|
|
||||||
|
dbBuilder.UseNpgsql(connectionString, options =>
|
||||||
|
{
|
||||||
|
options.MigrationsAssembly("SoaFE.Portal.Data");
|
||||||
|
options.MigrationsHistoryTable("_migrations_history");
|
||||||
|
options.MapSoaFEPortalEnums();
|
||||||
|
});
|
||||||
|
|
||||||
|
return new(dbBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Server/Features/App.razor
Normal file
23
src/Server/Features/App.razor
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<ResourcePreloader />
|
||||||
|
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||||
|
<link rel="stylesheet" href="@Assets["SoaFE.Portal.Server.styles.css"]" />
|
||||||
|
<ImportMap />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<HeadOutlet @rendermode="InteractiveWebAssembly" />
|
||||||
|
<RadzenTheme Theme="material-dark" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes @rendermode="InteractiveWebAssembly" />
|
||||||
|
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||||
|
<script src="@Assets["_content/Radzen.Blazor/Radzen.Blazor.js"]"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using AspNet.Security.OAuth.Discord;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SoaFE.Portal.Core;
|
||||||
|
using SoaFE.Portal.Data.Models;
|
||||||
|
|
||||||
|
namespace SoaFE.Portal.Server.Features.Auth.Extensions;
|
||||||
|
|
||||||
|
public static class EndpointRouteBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapAuth(this IEndpointRouteBuilder endpoints)
|
||||||
|
{
|
||||||
|
endpoints.MapGet(
|
||||||
|
pattern: "/auth/sign-in",
|
||||||
|
handler: async (HttpContext context) =>
|
||||||
|
{
|
||||||
|
await context.ChallengeAsync(
|
||||||
|
scheme: DiscordAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
properties: new()
|
||||||
|
{
|
||||||
|
RedirectUri = "/auth/signin-discord-callback"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.AllowAnonymous();
|
||||||
|
|
||||||
|
endpoints.MapGet(
|
||||||
|
pattern: "/auth/sign-out",
|
||||||
|
handler: async (HttpContext context) =>
|
||||||
|
{
|
||||||
|
await context.SignOutAsync();
|
||||||
|
context.Response.Redirect("/");
|
||||||
|
})
|
||||||
|
.AllowAnonymous();
|
||||||
|
|
||||||
|
endpoints.MapGet(
|
||||||
|
pattern: "/auth/signin-discord-callback",
|
||||||
|
handler: async (
|
||||||
|
HttpContext context,
|
||||||
|
UserManager<User> userManager,
|
||||||
|
SignInManager<User> signInManager
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
AuthenticateResult? externalAuth = await context.AuthenticateAsync(DiscordAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
if (!externalAuth.Succeeded)
|
||||||
|
{
|
||||||
|
return Results.Redirect("/auth/sign-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
ClaimsPrincipal principal = externalAuth.Principal;
|
||||||
|
long userId = long.Parse(principal.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||||
|
string userName = principal.FindFirst(ClaimTypes.Name)!.Value;
|
||||||
|
string? avatarUrl = principal.FindFirst(Constants.DiscordAvatarClaim)?.Value;
|
||||||
|
User? user = await userManager.Users
|
||||||
|
.Where(x => x.Id == userId)
|
||||||
|
.SingleOrDefaultAsync(context.RequestAborted);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
user = new()
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
UserName = userName,
|
||||||
|
NormalizedUserName = userName.Normalize().ToUpperInvariant(),
|
||||||
|
AvatarUrl = avatarUrl,
|
||||||
|
LastLogin = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
await userManager.CreateAsync(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.UserName = userName;
|
||||||
|
user.AvatarUrl = avatarUrl;
|
||||||
|
user.LastLogin = DateTimeOffset.UtcNow;
|
||||||
|
await userManager.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Claim[] claims = [
|
||||||
|
new(Constants.DiscordAvatarClaim, user.AvatarUrl ?? string.Empty)
|
||||||
|
];
|
||||||
|
|
||||||
|
await signInManager.SignInWithClaimsAsync(
|
||||||
|
user: user,
|
||||||
|
isPersistent: true,
|
||||||
|
additionalClaims: claims);
|
||||||
|
|
||||||
|
return Results.Redirect("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/Server/Features/Home/Pages/Error.razor
Normal file
36
src/Server/Features/Home/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more 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>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[CascadingParameter]
|
||||||
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||||
|
}
|
||||||
1
src/Server/Features/Home/Pages/_Imports.razor
Normal file
1
src/Server/Features/Home/Pages/_Imports.razor
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@namespace SoaFE.Portal.Server.Features.Home.Pages
|
||||||
21
src/Server/Features/_Imports.razor
Normal file
21
src/Server/Features/_Imports.razor
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@namespace SoaFE.Portal.Server.Features
|
||||||
|
|
||||||
|
@using Blazilla
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using Radzen
|
||||||
|
@using Radzen.Blazor
|
||||||
|
@using SoaFE.Portal.Client
|
||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using System.Text.Json
|
||||||
123
src/Server/Program.cs
Normal file
123
src/Server/Program.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using AspNet.Security.OAuth.Discord;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SoaFE.Portal.Core;
|
||||||
|
using SoaFE.Portal.Core.Extensions;
|
||||||
|
using SoaFE.Portal.Data;
|
||||||
|
using SoaFE.Portal.Data.Extensions;
|
||||||
|
using SoaFE.Portal.Data.Models;
|
||||||
|
using SoaFE.Portal.Server.Features;
|
||||||
|
using SoaFE.Portal.Server.Features.Auth.Extensions;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Configure Blazor
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveWebAssemblyComponents()
|
||||||
|
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
|
||||||
|
builder.Services.AddSoaFEPortalCore();
|
||||||
|
builder.Services.AddLocalization();
|
||||||
|
|
||||||
|
// Configure Authentication
|
||||||
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultScheme = IdentityConstants.ApplicationScheme;
|
||||||
|
options.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
|
||||||
|
options.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
|
||||||
|
options.DefaultChallengeScheme = DiscordAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddDiscord(options =>
|
||||||
|
{
|
||||||
|
options.CallbackPath = "/signin-discord";
|
||||||
|
options.ClientId = builder.Configuration["OAuth:ClientId"]!;
|
||||||
|
options.ClientSecret = builder.Configuration["OAuth:ClientSecret"]!;
|
||||||
|
options.Scope.Add("identify");
|
||||||
|
options.ClaimActions.MapCustomJson(Constants.DiscordAvatarClaim, user =>
|
||||||
|
{
|
||||||
|
return string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"https://cdn.discordapp.com/avatars/{0}/{1}.{2}",
|
||||||
|
user.GetString("id"),
|
||||||
|
user.GetString("avatar"),
|
||||||
|
user.GetString("avatar")?.StartsWith("a_") ?? false
|
||||||
|
? "gif"
|
||||||
|
: "png"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Authorization
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// Configure EF Core
|
||||||
|
builder.Services.AddDbContextFactory<SoaFEDbContext>(options =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("SoaFEPortalDb");
|
||||||
|
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
// Use detailed errors and sensitive data logging only in development
|
||||||
|
options.EnableDetailedErrors();
|
||||||
|
options.EnableSensitiveDataLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.Environment.IsProduction())
|
||||||
|
{
|
||||||
|
options.EnableThreadSafetyChecks(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||||
|
{
|
||||||
|
npgsqlOptions.EnableRetryOnFailure(
|
||||||
|
maxRetryCount: 5,
|
||||||
|
maxRetryDelay: TimeSpan.FromSeconds(10),
|
||||||
|
errorCodesToAdd: null);
|
||||||
|
npgsqlOptions.MigrationsAssembly("SoaFE.Portal.Data");
|
||||||
|
npgsqlOptions.MigrationsHistoryTable("_migrations_history");
|
||||||
|
npgsqlOptions.MapSoaFEPortalEnums();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure Identity
|
||||||
|
builder.Services.AddIdentity<User, Role>()
|
||||||
|
.AddEntityFrameworkStores<SoaFEDbContext>()
|
||||||
|
.AddUserManager<UserManager<User>>()
|
||||||
|
.AddSignInManager<SignInManager<User>>();
|
||||||
|
|
||||||
|
// Configure Features
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseWebAssemblyDebugging();
|
||||||
|
app.UseMigrationsEndPoint();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
// 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.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
app.MapStaticAssets();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapAuth();
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
|
.AddAdditionalAssemblies(typeof(SoaFE.Portal.Client._Imports).Assembly);
|
||||||
|
|
||||||
|
app.Run();
|
||||||
24
src/Server/Server.csproj
Normal file
24
src/Server/Server.csproj
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<RootNamespace>$(Namespace).Server</RootNamespace>
|
||||||
|
<AssemblyName>$(Namespace).Server</AssemblyName>
|
||||||
|
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||||
|
<UserSecretsId>dffd14fa-856a-4713-8507-639dfd10dec0</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../Client/Client.csproj" />
|
||||||
|
<ProjectReference Include="../Data/Data.csproj" />
|
||||||
|
<PackageReference Include="AspNet.Security.OAuth.Discord" Version="10.*" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(DotNetVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="$(DotNetVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Features/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
src/Server/appsettings.Development.json
Normal file
8
src/Server/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/Server/appsettings.json
Normal file
9
src/Server/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
60
src/Server/wwwroot/app.css
Normal file
60
src/Server/wwwroot/app.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
html, body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #006bb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.darker-border-checkbox.form-check-input {
|
||||||
|
border-color: #929292;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
BIN
src/Server/wwwroot/favicon.png
Normal file
BIN
src/Server/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user