Compare commits

...

No commits in common. "main" and "features/feature_001" have entirely different histories.

28 changed files with 775 additions and 25 deletions

72
.gitignore vendored
View File

@ -1,8 +1,5 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
@ -10,6 +7,7 @@
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@ -22,17 +20,37 @@ mono_crash.*
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Dd]ebug/x64/
[Dd]ebugPublic/x64/
[Rr]elease/x64/
[Rr]eleases/x64/
bin/x64/
obj/x64/
[Dd]ebug/x86/
[Dd]ebugPublic/x86/
[Rr]elease/x86/
[Rr]eleases/x86/
bin/x86/
obj/x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
@ -44,12 +62,16 @@ Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Approval Tests result files
*.received.*
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
@ -76,6 +98,7 @@ StyleCopReport.xml
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
@ -156,6 +179,7 @@ coverage*.info
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
@ -297,9 +321,6 @@ node_modules/
# 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
@ -317,22 +338,22 @@ node_modules/
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
**/.fake/
# CodeRush personal settings
.cr/personal
**/.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
#tools/**
#!tools/packages.config
# Tabs Studio
*.tss
@ -354,15 +375,19 @@ ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
MSBuild_Logs/
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
**/.mfractor/
# Local History for Visual Studio
.localhistory/
**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
@ -374,7 +399,7 @@ healthchecksdb
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
@ -385,18 +410,17 @@ FodyWeavers.xsd
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
*.msp

View File

@ -1,2 +1,38 @@
# chat_application
# 💬 Chat Application
A modern chat application enabling real-time messaging, group conversations (salons), and role-based access control.
---
## 🚀 Features
- User registration and authentication (Logto)
- Real-time messaging between users
- Group conversations (salons) and private chats
- Role and permission management (admin panel)
- User invitations to salons
- Responsive Angular frontend
- Secure .NET API backend
---
## 🛠️ Tech Stacks
- **Frontend:** Angular
- **Backend:** .NET API
- **Authentication:** Logto (Identity Provider)
- **Hosting:** Hetzner Cloud
---
## 📦 Getting Started
### Prerequisites
- Node.js & npm (for Angular)
- .NET SDK (for backend)
- Logto account (for authentication)
## 📄 License
This project is licensed under the QAP License.

132
doc/feature_001.md Normal file
View File

@ -0,0 +1,132 @@
# Features
## Personas
- **Alice**: New user who wants to join the platform to chat with friends.
- **Bob**: Regular user, member of several salons, likes to organize group conversations.
- **Claire**: Administrator, responsible for managing roles and permissions.
- **David**: Invited user, discovers the platform via a salon invitation.
## User Registration
**Persona:** Alice
**Scenario:** User registers into the application
- Given Alice is on the registration page
- When she provides valid details (username, email, password)
- And she clicks "Register"
- Then the username, email and password are validated by the system
- And her account is created
- And she receives a confirmation message
- And she is redirected to the login page
- And she receives a welcome email
- When she tries to register with an existing email
- Then the system displays an error message
## User Login
**Persona:** Alice
**Scenario:** User logs into the application
- Given Alice is on the login page
- When she enters valid credentials (email/username and password)
- And she clicks "Login"
- Then the credentials are validated by the system
- And she is granted access to the application
- When she enters an invalid password
- Then the system displays an error message
- When she forgets her password
- Then she can request a password reset link
## Role Management
**Persona:** Claire
**Scenario:** Admin creates and assigns roles
- Given Claire is logged in as an admin
- When she navigates to the roles management page
- And she creates a new role or edits an existing one
- Then the role is saved
- And she can assign or unassign it to the users
- When she tries to assign a role to herself
- Then the system prevents self-assignment for certain roles (e.g., SuperAdmin)
- When she deletes a role
- Then the system asks for confirmation and updates all affected users
## Permission Management
**Persona:** Claire
**Scenario:** Admin manages permissions for roles
- Given Claire is logged in as an admin
- When she navigates to the permissions management page
- And she assigns or removes permissions for a role
- Then the permissions are updated
- And users with that role have the correct accesses
- When she tries to remove all permissions from a role
- Then the system warns about potential loss of access
## Send a Message
**Persona:** Bob
**Scenario:** User sends a message in a conversation
- Given Bob is logged in
- And he is in an existing conversation with other users
- When he types a message
- And he clicks "Send"
- Then the message appears in the conversation for all participants
- When he sends an empty message
- Then the system prevents sending and shows a warning
- When he sends a message with an attachment
- Then the attachment is uploaded and visible to all participants
## Read Messages
**Persona:** Alice
**Scenario:** User reads messages in a conversation
- Given Alice is logged in
- And she has joined a conversation
- When another user sends a message
- Then the message appears in the conversation in real time
- When she scrolls up, older messages are loaded
- When she marks a message as read
- Then the sender is notified (if enabled)
## Create a Conversation
**Persona:** Bob
**Scenario:** User creates a conversation with one or more users
- Given Bob is logged in
- When he clicks "New Conversation"
- And he selects one or more users
- And he clicks "Create"
- Then a new conversation is created
- And the selected users are added to the conversation
- When he tries to create a conversation with himself only
- Then the system prevents it and shows a message
## Salon Creation
**Persona:** Bob
**Scenario:** User creates a new salon
- Given Bob is logged in
- When he navigates to the "Create Salon" page
- And he provides a salon name
- And he clicks "Create"
- Then a new salon is created
- And Bob is added as a member of the salon
- When he tries to create a salon with a name that already exists
- Then the system displays an error message
## Invite Users to a Salon
**Persona:** Bob, David
**Scenario:** User invites others to join a salon
- Given Bob is logged in
- And he is a member of an existing salon
- When he selects other users to invite (including David)
- And he clicks "Invite"
- Then the selected users receive an invitation
- And they can join the salon once they accept
- When David accepts the invitation
- Then he becomes a member of the salon
- When David declines the invitation
- Then Bob is notified of the refusal
# Technical specifications
- **.NET**: Main framework for backend, API, and business logic.
- **Angular**: Framework used for developing the interactive web frontend.
- **Docker**: Containerization of services to facilitate deployment and portability.
- **Logto**: Authentication and identity management solution to secure access.
- **SQLite**: Lightweight database, ideal for local storage or testing environments.
- **Hetzner**: Hosting provider for production deployment (servers, cloud).

View File

@ -0,0 +1,7 @@
{
"Logto": {
"Endpoint": "https://en7hny.logto.app/",
"AppId": "n863z6tazsbjtn8b113x7",
"AppSecret": "1J4FpfWaEzPsGKALpBnhT5dqoYY19Bta"
}
}

View File

@ -0,0 +1,22 @@
using chat.Application.Authentication.Commands.Register;
using chat.Application.Authentication.Dtos;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
namespace chat.API.AuthController;
[ApiController]
[Route("chat_api/auth")]
[Authorize]
public class AuthController(IMediator mediator) : ControllerBase
{
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult<RegisterResponseDto>> Register([FromBody] RegisterCommand command)
{
RegisterResponseDto dto = await mediator.Send(command);
return Ok(dto);
}
}

View File

@ -0,0 +1,43 @@
using chat.API.Middlewares;
using Microsoft.OpenApi.Models;
namespace chat.API.Extensions;
public static class WebApplicationBuilderExtensions
{
public static void AddPresentation(this WebApplicationBuilder builder)
{
builder.Services.AddAuthentication();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
// Définit le schéma de sécurité pour l'authentification Bearer (JWT)
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme()
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
});
// Ajoute l'exigence de sécurité pour que Swagger utilise le schéma Bearer
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "bearerAuth"
}
},
[]
}
});
});
builder.Services.AddScoped<ErrorHandlingMiddleware>();
}
}

View File

@ -0,0 +1,28 @@
using chat.Domain.Exceptions;
namespace chat.API.Middlewares;
public class ErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware> logger) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (HttpResponseException ex)
{
logger.LogWarning(ex, "HTTP {StatusCode}: {Message}", ex.StatusCode, ex.Message);
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error occurred");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
}
}
}

29
src/chat.API/Program.cs Normal file
View File

@ -0,0 +1,29 @@
using chat.API.Extensions;
using chat.API.Middlewares;
using chat.Application.Extensions;
using chat.Infrastructure.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.AddPresentation();
builder.Services.AddApplication();
builder.Services.AddInfrastructure();
var app = builder.Build();
app.UseMiddleware<ErrorHandlingMiddleware>();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5297",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7169;http://localhost:5297",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Logto": {
"Endpoint": "https://en7hny.logto.app/", // URL de base de ton tenant Logto
"Issuer": "https://en7hny.logto.app/oidc", // URL pour valider les JWT (utilisé par le middleware auth)
"Audience": "https://localhost:7169", // Identifiant de ton API (pour valider les tokens)
"ManagementApi": "https://en7hny.logto.app/api", // URL de l'API Management Logto (pour créer des users)
"M2M": {
"AppId": "", // Client ID de l'app M2M (pour obtenir un token Management) dans user-secrets
"AppSecret": "" // Client Secret de l'app M2M dans user-secrets
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>7224f703-f9b3-4ef2-a34d-b2ba0038d160</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\chat.Application\chat.Application.csproj" />
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
<ProjectReference Include="..\chat.Infrastructure\chat.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
using chat.Application.Authentication.Dtos;
using MediatR;
namespace chat.Application.Authentication.Commands.Register;
public class RegisterCommand : IRequest<RegisterResponseDto>
{
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}

View File

@ -0,0 +1,28 @@
using AutoMapper;
using chat.Application.Authentication.Dtos;
using chat.Domain.Entities;
using chat.Domain.Interfaces;
using MediatR;
using Microsoft.Extensions.Logging;
namespace chat.Application.Authentication.Commands.Register;
public class RegisterCommandHandler(
ILogger<RegisterCommandHandler> logger,
IMapper mapper,
ILogtoAuthService authRepository
) : IRequestHandler<RegisterCommand, RegisterResponseDto>
{
public async Task<RegisterResponseDto> Handle(RegisterCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("Creating a new user with the following credentials {Username} {Email}", request.Username, request.Email);
var user = mapper.Map<User>(request);
var userRegistered = await authRepository.Register(user , cancellationToken);
if (userRegistered == null)
{
throw new Exception("Registration failed");
}
var response = mapper.Map<RegisterResponseDto>(userRegistered);
return response;
}
}

View File

@ -0,0 +1,19 @@
using FluentValidation;
namespace chat.Application.Authentication.Commands.Register;
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
{
public RegisterCommandValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required.")
.MinimumLength(3).WithMessage("Username must be at least 3 characters long.");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required.")
.MinimumLength(6).WithMessage("Password must be at least 6 characters long.");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required.")
.EmailAddress().WithMessage("A valid email address is required.");
}
}

View File

@ -0,0 +1,24 @@
using AutoMapper;
using chat.Application.Authentication.Commands.Register;
using chat.Domain.Entities;
namespace chat.Application.Authentication.Dtos
{
public class AuthProfile : Profile
{
public AuthProfile()
{
CreateMap<RegisterCommand, User>()
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.Username))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.ForMember(dest => dest.Password, opt => opt.MapFrom(src => src.Password))
.ReverseMap();
CreateMap<RegisterResponseDto, User>()
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.Username))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
.ForMember(dest => dest.Password, opt => opt.MapFrom(src => src.Password))
.ReverseMap();
}
}
}

View File

@ -0,0 +1,8 @@
namespace chat.Application.Authentication.Dtos;
public class RegisterResponseDto
{
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}

View File

@ -0,0 +1,20 @@
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
namespace chat.Application.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddApplication(this IServiceCollection services)
{
var applicationAssembly = typeof(ServiceCollectionExtensions).Assembly;
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(applicationAssembly));
services.AddAutoMapper(cfg => cfg.AddMaps(applicationAssembly));
services.AddValidatorsFromAssembly(applicationAssembly)
.AddFluentValidationAutoValidation();
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="SharpGrip.FluentValidation.AutoValidation.Mvc" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace chat.Domain.Entities;
public class User
{
public string Username { get; set; } = default!;
public string Email { get; set; } = default!;
public string Password { get; set; } = default!;
}

View File

@ -0,0 +1,11 @@
namespace chat.Domain.Exceptions;
public class HttpResponseException : Exception
{
public int StatusCode { get; }
public HttpResponseException(int statusCode, string message) : base(message)
{
StatusCode = statusCode;
}
}

View File

@ -0,0 +1,9 @@
using chat.Domain.Entities;
namespace chat.Domain.Interfaces
{
public interface ILogtoAuthService
{
Task<User?> Register(User request, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
using chat.Domain.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace chat.Infrastructure.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddInfrastructure(this IServiceCollection services)
{
services.AddHttpClient<LogtoAuthService>();
services.AddScoped<ILogtoAuthService, LogtoAuthService>();
}
}

View File

@ -0,0 +1,115 @@
using chat.Domain.Entities;
using chat.Domain.Interfaces;
using chat.Domain.Exceptions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
namespace chat.Infrastructure;
public class LogtoAuthService(
ILogger<LogtoAuthService> _logger,
HttpClient _httpClient,
IConfiguration _configuration) : ILogtoAuthService
{
public async Task<User?> Register(User request, CancellationToken cancellationToken)
{
var token = await RetrieveManagementToken(cancellationToken);
var managementApiUrl = _configuration["Logto:ManagementApi"]!;
var createUserEndpoint = $"{managementApiUrl}/users";
var createUserRequest = new
{
username = request.Username,
primaryEmail = request.Email,
password = request.Password,
name = request.Username
};
var requestMessage = new HttpRequestMessage(HttpMethod.Post, createUserEndpoint)
{
Content = JsonContent.Create(createUserRequest),
Headers =
{
{ "Authorization", $"Bearer {token}" }
}
};
var response = await _httpClient.SendAsync(requestMessage, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken);
throw new HttpResponseException((int)response.StatusCode, error);
}
var logtoUser = await response.Content.ReadFromJsonAsync<LogtoUserResponse>(cancellationToken);
if (logtoUser == null)
{
throw new InvalidOperationException("Failed to deserialize Logto user response");
}
_logger.LogInformation("User created successfully in Logto. UserId: {UserId}, Username: {Username}",
logtoUser.Id, logtoUser.Username);
return new User
{
Username = logtoUser.Username ?? request.Username,
Email = logtoUser.PrimaryEmail ?? request.Email,
Password = "********"
};
}
private async Task<string> RetrieveManagementToken(CancellationToken cancellationToken)
{
var tokenEndpoint = $"{_configuration["Logto:Issuer"]}/token";
var managementApiUrl = _configuration["Logto:ManagementApi"]!;
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = _configuration["Logto:M2M:AppId"]!,
["client_secret"] = _configuration["Logto:M2M:AppSecret"]!,
["resource"] = managementApiUrl,
["scope"] = "all"
});
var response = await _httpClient.PostAsync(tokenEndpoint, tokenRequest, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken);
throw new HttpResponseException((int)response.StatusCode, $"{error}");
}
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken);
if (tokenResponse?.AccessToken == null)
{
throw new InvalidOperationException("M2M token response is invalid");
}
_logger.LogInformation("M2M token obtained successfully. Type: {TokenType}, Expires in: {ExpiresIn}s, scope: {Scope}",
tokenResponse.TokenType,
tokenResponse.ExpiresIn,
tokenResponse.Scope);
return tokenResponse.AccessToken;
}
private record TokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("token_type")] string TokenType,
[property: JsonPropertyName("expires_in")] int ExpiresIn,
[property: JsonPropertyName("scope")] string Scope
);
private record LogtoUserResponse(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("username")] string? Username,
[property: JsonPropertyName("primaryEmail")] string? PrimaryEmail,
[property: JsonPropertyName("name")] string? Name
);
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
</ItemGroup>
</Project>

40
src/chat_application.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.3.11312.210 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Infrastructure", "chat.Infrastructure\chat.Infrastructure.csproj", "{C4382C35-1776-4F39-BF67-2BE197AB4283}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Domain", "chat.Domain\chat.Domain.csproj", "{3ED98938-5F3B-49D8-824B-95AFAF328A1F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.API", "chat.API\chat.API.csproj", "{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Application", "chat.Application\chat.Application.csproj", "{619A7FFA-4796-4731-93A3-4AFB002ABD3F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Release|Any CPU.Build.0 = Release|Any CPU
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Release|Any CPU.Build.0 = Release|Any CPU
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Release|Any CPU.Build.0 = Release|Any CPU
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

0
tests/test.txt Normal file
View File