feat(auth): adds basic endpoints: register, login, forgot password, reset password
This commit is contained in:
107
docs/architecture.md
Normal file
107
docs/architecture.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# TrakQR Architecture Guidelines
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Modular Monolith
|
||||
The application is structured as a modular monolith where each feature/domain is self-contained but runs in a single deployable unit.
|
||||
|
||||
This provides:
|
||||
- Clear boundaries between features
|
||||
- Easy refactoring to microservices if needed later
|
||||
- Simplified deployment and operations
|
||||
|
||||
### 2. Vertical Slice Architecture
|
||||
Code is organized by **feature** (vertical slices), not by technical layer (horizontal).
|
||||
|
||||
Each feature contains everything it needs:
|
||||
- Endpoint definitions
|
||||
- Request/Response models
|
||||
- Business logic
|
||||
- Validators
|
||||
|
||||
```
|
||||
/Features
|
||||
/Auth
|
||||
/Endpoints
|
||||
SmallEndpoint.cs (contains Request, Response, Validator and Endpoint)
|
||||
/Register
|
||||
Endpoint.cs
|
||||
Request.cs
|
||||
Response.cs
|
||||
Validator.cs
|
||||
/Login
|
||||
...
|
||||
/Links
|
||||
/Create
|
||||
...
|
||||
/List
|
||||
...
|
||||
/QRCodes
|
||||
...
|
||||
```
|
||||
|
||||
### 3. Minimal API with FastEndpoints
|
||||
We use [FastEndpoints](https://fast-endpoints.com/) instead of traditional MVC Controllers because:
|
||||
- Better performance (no reflection-based model binding)
|
||||
- Cleaner, more focused endpoint classes
|
||||
- Built-in validation with FluentValidation
|
||||
- Request/Response DTOs are co-located with endpoints
|
||||
- Easier testing
|
||||
|
||||
### 4. No Traditional Controllers
|
||||
**DO NOT** use `[ApiController]` or `ControllerBase`. All HTTP endpoints must be FastEndpoints.
|
||||
|
||||
## Module Structure
|
||||
|
||||
Each feature module should be **fully self-contained**. All code related to a feature lives within that feature's folder:
|
||||
|
||||
```
|
||||
/Features/{Module}/
|
||||
- {Module}Responses.cs # Shared response DTOs for this module
|
||||
- {Module}Settings.cs # Configuration classes for this module
|
||||
/{Operation}/
|
||||
- Endpoint.cs # The FastEndpoint class
|
||||
- Request.cs # Input DTO
|
||||
- Validator.cs # FluentValidation rules
|
||||
```
|
||||
|
||||
**Example - Auth module:**
|
||||
```
|
||||
/Features/Auth/
|
||||
- AuthResponses.cs # AuthResponse, UserInfo, MessageResponse
|
||||
- JwtSettings.cs # JWT configuration
|
||||
/Register/
|
||||
- Endpoint.cs
|
||||
- Request.cs
|
||||
- Validator.cs
|
||||
/Login/
|
||||
- Endpoint.cs
|
||||
- Request.cs
|
||||
- Validator.cs
|
||||
```
|
||||
|
||||
For simple operations, business logic lives directly in the Endpoint class. For complex logic, add a `Handler.cs` or `Service.cs` within the same feature folder.
|
||||
|
||||
## Shared Infrastructure
|
||||
|
||||
Only truly cross-cutting infrastructure goes outside Features:
|
||||
- `/Data` - DbContext, migrations (shared database access)
|
||||
- `/Models` - EF Core entities (shared domain model)
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
- Use constructor injection
|
||||
- Register services in `Program.cs` or feature-specific extension methods
|
||||
- Prefer scoped services for request-specific work
|
||||
|
||||
## Validation
|
||||
|
||||
- Use FluentValidation via FastEndpoints' built-in support
|
||||
- Validate at the endpoint level, not in services
|
||||
- Return 400 Bad Request with structured error responses
|
||||
|
||||
## Authentication
|
||||
|
||||
- JWT Bearer tokens for API authentication
|
||||
- Claims-based authorization
|
||||
- User ID extracted from JWT `sub` claim
|
||||
13
src/.idea/.idea.src/.idea/.gitignore
generated
vendored
Normal file
13
src/.idea/.idea.src/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/modules.xml
|
||||
/.idea.src.iml
|
||||
/contentModel.xml
|
||||
/projectSettingsUpdater.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
4
src/.idea/.idea.src/.idea/encodings.xml
generated
Normal file
4
src/.idea/.idea.src/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
8
src/.idea/.idea.src/.idea/indexLayout.xml
generated
Normal file
8
src/.idea/.idea.src/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
6
src/.idea/.idea.src/.idea/vcs.xml
generated
Normal file
6
src/.idea/.idea.src/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
482
src/api.Tests/.gitignore
vendored
Normal file
482
src/api.Tests/.gitignore
vendored
Normal file
@@ -0,0 +1,482 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
50
src/api.Tests/ApiWebApplicationFactory.cs
Normal file
50
src/api.Tests/ApiWebApplicationFactory.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using api.Data;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace Api.Tests;
|
||||
|
||||
public sealed class ApiWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder("postgres:latest")
|
||||
.Build();
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
// Remove existing DbContext registration
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
|
||||
|
||||
if (descriptor != null)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
// Add DbContext with Testcontainers connection string
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(_postgres.GetConnectionString()));
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _postgres.StartAsync();
|
||||
|
||||
// Run migrations
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public new async Task DisposeAsync()
|
||||
{
|
||||
await _postgres.DisposeAsync();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
225
src/api.Tests/AuthControllerTests.cs
Normal file
225
src/api.Tests/AuthControllerTests.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using api.Features.Auth.Common;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Api.Tests;
|
||||
|
||||
public class AuthControllerTests(ApiWebApplicationFactory factory)
|
||||
: IClassFixture<ApiWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client = factory.CreateClient();
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithValidCredentials_ReturnsTokenAndUser()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "newuser@example.com", Password = "password123" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Token.Should().NotBeNullOrEmpty();
|
||||
result.User.Email.Should().Be("newuser@example.com");
|
||||
result.User.IsVerified.Should().BeFalse();
|
||||
result.ExpiresAt.Should().BeAfter(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithDuplicateEmail_ReturnsConflict()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "duplicate@example.com", Password = "password123" };
|
||||
|
||||
// First registration
|
||||
await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Act - try to register again
|
||||
var response = await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("Email already registered");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithInvalidEmail_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "not-an-email", Password = "password123" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithShortPassword_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "shortpw@example.com", Password = "short" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithValidCredentials_ReturnsToken()
|
||||
{
|
||||
// Arrange
|
||||
var email = "logintest@example.com";
|
||||
var password = "password123";
|
||||
|
||||
await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = password });
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = password });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Token.Should().NotBeNullOrEmpty();
|
||||
result.User.Email.Should().Be(email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithWrongPassword_ReturnsUnauthorized()
|
||||
{
|
||||
// Arrange
|
||||
var email = "wrongpw@example.com";
|
||||
await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "correctpassword" });
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/login", new { Email = email, Password = "wrongpassword" });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("Invalid email or password");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_WithNonExistentEmail_ReturnsUnauthorized()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "nonexistent@example.com", Password = "password123" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/login", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("Invalid email or password");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForgotPassword_WithAnyEmail_ReturnsSuccessMessage()
|
||||
{
|
||||
// Arrange - using a non-existent email to verify we don't leak info
|
||||
var request = new { Email = "anyone@example.com" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/forgot", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("If the email exists, a reset link will be sent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForgotPassword_WithExistingEmail_ReturnsSuccessMessage()
|
||||
{
|
||||
// Arrange
|
||||
var email = "forgotpw@example.com";
|
||||
await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = "password123" });
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/forgot", new { Email = email });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("If the email exists, a reset link will be sent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetPassword_ReturnsNotImplemented()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Token = "some-token", NewPassword = "newpassword123" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/reset", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MessageResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.Message.Should().Be("Password reset is not yet available");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_CreatesDefaultWorkspace()
|
||||
{
|
||||
// Arrange
|
||||
var request = new { Email = "workspace@example.com", Password = "password123" };
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/auth/register", request);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
||||
result.Should().NotBeNull();
|
||||
result!.User.Id.Should().NotBeEmpty();
|
||||
// The workspace is created but not returned in auth response
|
||||
// This could be verified with a separate workspaces endpoint test
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_IsCaseInsensitive()
|
||||
{
|
||||
// Arrange
|
||||
var email = "CaseTEST@Example.COM";
|
||||
var password = "password123";
|
||||
|
||||
await _client.PostAsJsonAsync("/auth/register", new { Email = email, Password = password });
|
||||
|
||||
// Act - login with different casing
|
||||
var response = await _client.PostAsJsonAsync("/auth/login",
|
||||
new { Email = "casetest@example.com", Password = password });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
||||
result!.User.Email.Should().Be("casetest@example.com");
|
||||
}
|
||||
}
|
||||
28
src/api.Tests/api.Tests.csproj
Normal file
28
src/api.Tests/api.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\api\api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,20 +1,17 @@
|
||||
using Api.Models;
|
||||
using api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Api.Data;
|
||||
namespace api.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: DbContext(options)
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<Project> Projects => Set<Project>();
|
||||
public DbSet<Domain> Domains => Set<Domain>();
|
||||
public DbSet<ShortLink> ShortLinks => Set<ShortLink>();
|
||||
public DbSet<QRCodeDesign> QRCodeDesigns => Set<QRCodeDesign>();
|
||||
public DbSet<QRCodeDesign> QrCodeDesigns => Set<QRCodeDesign>();
|
||||
public DbSet<Event> Events => Set<Event>();
|
||||
public DbSet<Asset> Assets => Set<Asset>();
|
||||
|
||||
|
||||
15
src/api/Features/Auth/Common/AuthResponses.cs
Normal file
15
src/api/Features/Auth/Common/AuthResponses.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace api.Features.Auth.Common;
|
||||
|
||||
public record AuthResponse(
|
||||
string Token,
|
||||
DateTime ExpiresAt,
|
||||
UserInfo User
|
||||
);
|
||||
|
||||
public record UserInfo(
|
||||
Guid Id,
|
||||
string Email,
|
||||
bool IsVerified
|
||||
);
|
||||
|
||||
public record MessageResponse(string Message);
|
||||
55
src/api/Features/Auth/Endpoints/ForgotPasswordEndpoint.cs
Normal file
55
src/api/Features/Auth/Endpoints/ForgotPasswordEndpoint.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Security.Cryptography;
|
||||
using api.Data;
|
||||
using api.Features.Auth.Common;
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace api.Features.Auth.Endpoints;
|
||||
|
||||
public class ForgotPasswordRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ForgotPasswordValidator : Validator<ForgotPasswordRequest>
|
||||
{
|
||||
public ForgotPasswordValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email format");
|
||||
}
|
||||
}
|
||||
|
||||
public class ForgotPasswordEndpoint(AppDbContext db)
|
||||
: Endpoint<ForgotPasswordRequest, MessageResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/forgot");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ForgotPasswordRequest req, CancellationToken ct)
|
||||
{
|
||||
var normalizedEmail = req.Email.ToLowerInvariant();
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == normalizedEmail, ct);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
Logger.LogInformation("Password reset requested for non-existent email: {Email}", normalizedEmail);
|
||||
}
|
||||
else
|
||||
{
|
||||
var resetToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
||||
.Replace("+", "-").Replace("/", "_").TrimEnd('=');
|
||||
// TODO: Store reset token in database with expiration
|
||||
// TODO: Send email with reset link
|
||||
Logger.LogInformation("Password reset token generated for: {Email}, Token: {Token}", normalizedEmail, resetToken);
|
||||
}
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
await HttpContext.Response.SendAsync(new MessageResponse("If the email exists, a reset link will be sent"), 200, cancellation: ct);
|
||||
}
|
||||
}
|
||||
85
src/api/Features/Auth/Endpoints/LoginEndpoint.cs
Normal file
85
src/api/Features/Auth/Endpoints/LoginEndpoint.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using api.Data;
|
||||
using api.Features.Auth.Common;
|
||||
using api.Features.Auth.Settings;
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace api.Features.Auth.Endpoints;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class LoginValidator : Validator<LoginRequest>
|
||||
{
|
||||
public LoginValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email format");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required");
|
||||
}
|
||||
}
|
||||
|
||||
public class LoginEndpoint(AppDbContext db, IOptions<JwtSettings> jwtSettings)
|
||||
: Endpoint<LoginRequest, AuthResponse>
|
||||
{
|
||||
private readonly JwtSettings _jwtSettings = jwtSettings.Value;
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/login");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
|
||||
{
|
||||
var normalizedEmail = req.Email.ToLowerInvariant();
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == normalizedEmail, ct);
|
||||
|
||||
if (user == null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash))
|
||||
{
|
||||
await HttpContext.Response.SendAsync(new MessageResponse("Invalid email or password"), 401, cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("User logged in: {Email}", normalizedEmail);
|
||||
|
||||
var expiresAt = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes);
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.Issuer,
|
||||
audience: _jwtSettings.Audience,
|
||||
claims: claims,
|
||||
expires: expiresAt,
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
var response = new AuthResponse(
|
||||
Token: new JwtSecurityTokenHandler().WriteToken(token),
|
||||
ExpiresAt: expiresAt,
|
||||
User: new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue)
|
||||
);
|
||||
|
||||
await HttpContext.Response.SendAsync(response, 200, cancellation: ct);
|
||||
}
|
||||
}
|
||||
114
src/api/Features/Auth/Endpoints/RegisterEndpoint.cs
Normal file
114
src/api/Features/Auth/Endpoints/RegisterEndpoint.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using api.Data;
|
||||
using api.Features.Auth.Common;
|
||||
using api.Features.Auth.Settings;
|
||||
using api.Models;
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace api.Features.Auth.Endpoints;
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RegisterValidator : Validator<RegisterRequest>
|
||||
{
|
||||
public RegisterValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email format")
|
||||
.MaximumLength(255).WithMessage("Email must not exceed 255 characters");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required")
|
||||
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
|
||||
.MaximumLength(100).WithMessage("Password must not exceed 100 characters");
|
||||
}
|
||||
}
|
||||
|
||||
public class RegisterEndpoint(AppDbContext db, IOptions<JwtSettings> jwtSettings)
|
||||
: Endpoint<RegisterRequest, AuthResponse>
|
||||
{
|
||||
private readonly JwtSettings _jwtSettings = jwtSettings.Value;
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/register");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RegisterRequest req, CancellationToken ct)
|
||||
{
|
||||
var normalizedEmail = req.Email.ToLowerInvariant();
|
||||
|
||||
if (await db.Users.AnyAsync(u => u.Email == normalizedEmail, ct))
|
||||
{
|
||||
await HttpContext.Response.SendAsync(new MessageResponse("Email already registered"), 409, cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = normalizedEmail,
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
db.Users.Add(user);
|
||||
|
||||
var workspace = new Workspace
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OwnerUserId = user.Id,
|
||||
Name = "My Workspace",
|
||||
Plan = WorkspacePlan.Free,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
db.Workspaces.Add(workspace);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
Logger.LogInformation("User registered: {Email}", normalizedEmail);
|
||||
|
||||
var response = GenerateAuthResponse(user);
|
||||
await HttpContext.Response.SendAsync(response, 201, cancellation: ct);
|
||||
}
|
||||
|
||||
private AuthResponse GenerateAuthResponse(User user)
|
||||
{
|
||||
var expiresAt = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpirationMinutes);
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, user.Email),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.Issuer,
|
||||
audience: _jwtSettings.Audience,
|
||||
claims: claims,
|
||||
expires: expiresAt,
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new AuthResponse(
|
||||
Token: new JwtSecurityTokenHandler().WriteToken(token),
|
||||
ExpiresAt: expiresAt,
|
||||
User: new UserInfo(user.Id, user.Email, user.VerifiedAt.HasValue)
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/api/Features/Auth/Endpoints/ResetPasswordEndpoint.cs
Normal file
46
src/api/Features/Auth/Endpoints/ResetPasswordEndpoint.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using api.Features.Auth.Common;
|
||||
using FastEndpoints;
|
||||
using FluentValidation;
|
||||
|
||||
namespace api.Features.Auth.Endpoints;
|
||||
|
||||
public class ResetPasswordRequest
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ValidatorResetPassword : Validator<ResetPasswordRequest>
|
||||
{
|
||||
public ValidatorResetPassword()
|
||||
{
|
||||
RuleFor(x => x.Token)
|
||||
.NotEmpty().WithMessage("Token is required");
|
||||
|
||||
RuleFor(x => x.NewPassword)
|
||||
.NotEmpty().WithMessage("New password is required")
|
||||
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
|
||||
.MaximumLength(100).WithMessage("Password must not exceed 100 characters");
|
||||
}
|
||||
}
|
||||
|
||||
public class ResetPasswordEndpoint : Endpoint<ResetPasswordRequest, MessageResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/auth/reset");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ResetPasswordRequest req, CancellationToken ct)
|
||||
{
|
||||
// TODO: Implement password reset
|
||||
// 1. Look up token in database
|
||||
// 2. Verify token hasn't expired
|
||||
// 3. Get associated user
|
||||
// 4. Update password
|
||||
// 5. Invalidate token
|
||||
|
||||
await HttpContext.Response.SendAsync(new MessageResponse("Password reset is not yet available"), 400, cancellation: ct);
|
||||
}
|
||||
}
|
||||
9
src/api/Features/Auth/Settings/JwtSettings.cs
Normal file
9
src/api/Features/Auth/Settings/JwtSettings.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace api.Features.Auth.Settings;
|
||||
|
||||
public class JwtSettings
|
||||
{
|
||||
public required string Secret { get; set; }
|
||||
public required string Issuer { get; set; }
|
||||
public required string Audience { get; set; }
|
||||
public int ExpirationMinutes { get; set; } = 60;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Api.Data;
|
||||
using api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Api.Data;
|
||||
using api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
540
src/api/Migrations/20260127205418_RefactorAuth.Designer.cs
generated
Normal file
540
src/api/Migrations/20260127205418_RefactorAuth.Designer.cs
generated
Normal file
@@ -0,0 +1,540 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260127205418_refactor auth")]
|
||||
partial class RefactorAuth
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("api.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Mime")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("StorageKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Hostname")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("VerificationToken")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Hostname")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("Domains");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Event", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("CountryCode")
|
||||
.HasMaxLength(2)
|
||||
.HasColumnType("character varying(2)");
|
||||
|
||||
b.Property<string>("DedupeKey")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("IpHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid?>("QRCodeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Referrer")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid>("ShortLinkId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("QRCodeId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("ShortLinkId", "Timestamp");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Timestamp");
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid?>("LogoAssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ShortLinkId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StyleJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LogoAssetId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ShortLinkId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("QrCodeDesigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("DestinationUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid?>("DomainId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("DomainId", "Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime?>("VerifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<Guid>("OwnerUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Workspaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Domains")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Event", b =>
|
||||
{
|
||||
b.HasOne("api.Models.QRCodeDesign", "QRCode")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("QRCodeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.ShortLink", "ShortLink")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("ShortLinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("QRCode");
|
||||
|
||||
b.Navigation("ShortLink");
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.HasOne("api.Models.Asset", "LogoAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("LogoAssetId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.Project", "Project")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.ShortLink", "ShortLink")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("ShortLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("LogoAsset");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("ShortLink");
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.HasOne("api.Models.Domain", "Domain")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("DomainId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.Project", "Project")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Domain");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.HasOne("api.Models.User", "Owner")
|
||||
.WithMany("Workspaces")
|
||||
.HasForeignKey("OwnerUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.Navigation("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.Navigation("QRCodeDesigns");
|
||||
|
||||
b.Navigation("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("QRCodeDesigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.User", b =>
|
||||
{
|
||||
b.Navigation("Workspaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
|
||||
b.Navigation("Domains");
|
||||
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("Projects");
|
||||
|
||||
b.Navigation("QRCodeDesigns");
|
||||
|
||||
b.Navigation("ShortLinks");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/api/Migrations/20260127205418_RefactorAuth.cs
Normal file
204
src/api/Migrations/20260127205418_RefactorAuth.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RefactorAuth : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Events_QRCodeDesigns_QRCodeId",
|
||||
table: "Events");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QRCodeDesigns_Assets_LogoAssetId",
|
||||
table: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QRCodeDesigns_Projects_ProjectId",
|
||||
table: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QRCodeDesigns_ShortLinks_ShortLinkId",
|
||||
table: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QRCodeDesigns_Workspaces_WorkspaceId",
|
||||
table: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_QRCodeDesigns",
|
||||
table: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "QRCodeDesigns",
|
||||
newName: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QRCodeDesigns_WorkspaceId",
|
||||
table: "QrCodeDesigns",
|
||||
newName: "IX_QrCodeDesigns_WorkspaceId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QRCodeDesigns_ShortLinkId",
|
||||
table: "QrCodeDesigns",
|
||||
newName: "IX_QrCodeDesigns_ShortLinkId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QRCodeDesigns_ProjectId",
|
||||
table: "QrCodeDesigns",
|
||||
newName: "IX_QrCodeDesigns_ProjectId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QRCodeDesigns_LogoAssetId",
|
||||
table: "QrCodeDesigns",
|
||||
newName: "IX_QrCodeDesigns_LogoAssetId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_QrCodeDesigns",
|
||||
table: "QrCodeDesigns",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Events_QrCodeDesigns_QRCodeId",
|
||||
table: "Events",
|
||||
column: "QRCodeId",
|
||||
principalTable: "QrCodeDesigns",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QrCodeDesigns_Assets_LogoAssetId",
|
||||
table: "QrCodeDesigns",
|
||||
column: "LogoAssetId",
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QrCodeDesigns_Projects_ProjectId",
|
||||
table: "QrCodeDesigns",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QrCodeDesigns_ShortLinks_ShortLinkId",
|
||||
table: "QrCodeDesigns",
|
||||
column: "ShortLinkId",
|
||||
principalTable: "ShortLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QrCodeDesigns_Workspaces_WorkspaceId",
|
||||
table: "QrCodeDesigns",
|
||||
column: "WorkspaceId",
|
||||
principalTable: "Workspaces",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Events_QrCodeDesigns_QRCodeId",
|
||||
table: "Events");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QrCodeDesigns_Assets_LogoAssetId",
|
||||
table: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QrCodeDesigns_Projects_ProjectId",
|
||||
table: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QrCodeDesigns_ShortLinks_ShortLinkId",
|
||||
table: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_QrCodeDesigns_Workspaces_WorkspaceId",
|
||||
table: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_QrCodeDesigns",
|
||||
table: "QrCodeDesigns");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "QrCodeDesigns",
|
||||
newName: "QRCodeDesigns");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QrCodeDesigns_WorkspaceId",
|
||||
table: "QRCodeDesigns",
|
||||
newName: "IX_QRCodeDesigns_WorkspaceId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QrCodeDesigns_ShortLinkId",
|
||||
table: "QRCodeDesigns",
|
||||
newName: "IX_QRCodeDesigns_ShortLinkId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QrCodeDesigns_ProjectId",
|
||||
table: "QRCodeDesigns",
|
||||
newName: "IX_QRCodeDesigns_ProjectId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_QrCodeDesigns_LogoAssetId",
|
||||
table: "QRCodeDesigns",
|
||||
newName: "IX_QRCodeDesigns_LogoAssetId");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_QRCodeDesigns",
|
||||
table: "QRCodeDesigns",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Events_QRCodeDesigns_QRCodeId",
|
||||
table: "Events",
|
||||
column: "QRCodeId",
|
||||
principalTable: "QRCodeDesigns",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QRCodeDesigns_Assets_LogoAssetId",
|
||||
table: "QRCodeDesigns",
|
||||
column: "LogoAssetId",
|
||||
principalTable: "Assets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QRCodeDesigns_Projects_ProjectId",
|
||||
table: "QRCodeDesigns",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QRCodeDesigns_ShortLinks_ShortLinkId",
|
||||
table: "QRCodeDesigns",
|
||||
column: "ShortLinkId",
|
||||
principalTable: "ShortLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_QRCodeDesigns_Workspaces_WorkspaceId",
|
||||
table: "QRCodeDesigns",
|
||||
column: "WorkspaceId",
|
||||
principalTable: "Workspaces",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -17,12 +17,12 @@ namespace api.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.2")
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Api.Models.Asset", b =>
|
||||
modelBuilder.Entity("api.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -61,7 +61,7 @@ namespace api.Migrations
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Domain", b =>
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -100,7 +100,7 @@ namespace api.Migrations
|
||||
b.ToTable("Domains");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Event", b =>
|
||||
modelBuilder.Entity("api.Models.Event", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -164,7 +164,7 @@ namespace api.Migrations
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Project", b =>
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -190,7 +190,7 @@ namespace api.Migrations
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -232,10 +232,10 @@ namespace api.Migrations
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("QRCodeDesigns");
|
||||
b.ToTable("QrCodeDesigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.ShortLink", b =>
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -298,7 +298,7 @@ namespace api.Migrations
|
||||
b.ToTable("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.User", b =>
|
||||
modelBuilder.Entity("api.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -330,7 +330,7 @@ namespace api.Migrations
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Workspace", b =>
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -361,9 +361,9 @@ namespace api.Migrations
|
||||
b.ToTable("Workspaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Asset", b =>
|
||||
modelBuilder.Entity("api.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -372,9 +372,9 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Domain", b =>
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Domains")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -383,20 +383,20 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Event", b =>
|
||||
modelBuilder.Entity("api.Models.Event", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.QRCodeDesign", "QRCode")
|
||||
b.HasOne("api.Models.QRCodeDesign", "QRCode")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("QRCodeId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.ShortLink", "ShortLink")
|
||||
b.HasOne("api.Models.ShortLink", "ShortLink")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("ShortLinkId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -409,9 +409,9 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Project", b =>
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -420,24 +420,24 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.Asset", "LogoAsset")
|
||||
b.HasOne("api.Models.Asset", "LogoAsset")
|
||||
.WithMany()
|
||||
.HasForeignKey("LogoAssetId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.Project", "Project")
|
||||
b.HasOne("api.Models.Project", "Project")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.ShortLink", "ShortLink")
|
||||
b.HasOne("api.Models.ShortLink", "ShortLink")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("ShortLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("QRCodeDesigns")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -452,19 +452,19 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.ShortLink", b =>
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.Domain", "Domain")
|
||||
b.HasOne("api.Models.Domain", "Domain")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("DomainId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.Project", "Project")
|
||||
b.HasOne("api.Models.Project", "Project")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Api.Models.Workspace", "Workspace")
|
||||
b.HasOne("api.Models.Workspace", "Workspace")
|
||||
.WithMany("ShortLinks")
|
||||
.HasForeignKey("WorkspaceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -477,9 +477,9 @@ namespace api.Migrations
|
||||
b.Navigation("Workspace");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Workspace", b =>
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.HasOne("Api.Models.User", "Owner")
|
||||
b.HasOne("api.Models.User", "Owner")
|
||||
.WithMany("Workspaces")
|
||||
.HasForeignKey("OwnerUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
@@ -488,36 +488,36 @@ namespace api.Migrations
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Domain", b =>
|
||||
modelBuilder.Entity("api.Models.Domain", b =>
|
||||
{
|
||||
b.Navigation("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Project", b =>
|
||||
modelBuilder.Entity("api.Models.Project", b =>
|
||||
{
|
||||
b.Navigation("QRCodeDesigns");
|
||||
|
||||
b.Navigation("ShortLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
|
||||
modelBuilder.Entity("api.Models.QRCodeDesign", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.ShortLink", b =>
|
||||
modelBuilder.Entity("api.Models.ShortLink", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("QRCodeDesigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.User", b =>
|
||||
modelBuilder.Entity("api.Models.User", b =>
|
||||
{
|
||||
b.Navigation("Workspaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Api.Models.Workspace", b =>
|
||||
modelBuilder.Entity("api.Models.Workspace", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public enum AssetType
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public enum DomainStatus
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public enum EventType
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public class Project
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public class QRCodeDesign
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public enum ShortLinkStatus
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Api.Models;
|
||||
namespace api.Models;
|
||||
|
||||
public enum WorkspacePlan
|
||||
{
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
using Api.Data;
|
||||
using System.Text;
|
||||
using api.Data;
|
||||
using api.Features.Auth.Settings;
|
||||
using FastEndpoints;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection")));
|
||||
|
||||
// Configure JWT settings
|
||||
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()!;
|
||||
|
||||
// Configure authentication
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtSettings.Issuer,
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret))
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddFastEndpoints();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -25,4 +51,9 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseFastEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -7,8 +7,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="api.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="FastEndpoints" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -7,5 +7,8 @@
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"PostgresConnection": "Host=localhost;Port=5400;Database=trakqr;Username=sa;Password=P@ssword123!"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "dev-secret-key-min-32-characters-long-for-hmac256!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,11 @@
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"PostgresConnection": ""
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "",
|
||||
"Issuer": "TrakQR",
|
||||
"Audience": "TrakQR",
|
||||
"ExpirationMinutes": 60
|
||||
}
|
||||
}
|
||||
|
||||
5
src/src.sln.DotSettings.user
Normal file
5
src/src.sln.DotSettings.user
Normal file
@@ -0,0 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fcdd0beaf7beaf8366c0862f34fe40da30911084d957625ab31577851ee8cae7_003FPostgreSqlBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f24d9dca_002Dcc3a_002D42e4_002D8e9d_002D00aa5709be91/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;api.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<Project Location="/home/jbourdon/repos/trakqr/src/api.Tests" Presentation="&lt;api.Tests&gt;" />
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
4
src/src.slnx
Normal file
4
src/src.slnx
Normal file
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="api.Tests/api.Tests.csproj" />
|
||||
<Project Path="api/api.csproj" />
|
||||
</Solution>
|
||||
Reference in New Issue
Block a user