Track vscode setup

This commit is contained in:
Paul Schneider
2025-03-02 16:53:36 +00:00
parent 204bb80252
commit c351d71129
7 changed files with 279 additions and 199 deletions

1
.gitignore vendored
View File

@ -5,7 +5,6 @@
.gitignore .gitignore
.paket/ .paket/
.vscode/
.vs/ .vs/
.sass-cache/ .sass-cache/

88
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,88 @@
{
// Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
// Pointez pour afficher la description des attributs existants.
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
/* {
"name": ".NET Core Launch (web-client)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/sampleWebAsWebApiClient/bin/Debug/net8.0/sampleWebAsWebApiClient.dll>",
"args": [],
"cwd": "${workspaceFolder}/src/sampleWebAsWebApiClient",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Launch (Api)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-webapi",
"program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Api",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-web",
"program": "${workspaceFolder}/src/Yavsc/bin/Debug/net8.0/Yavsc.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Yavsc",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},*/
{
"name": "webApi",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/src/Api/Api.csproj"
},
{
"name": "webClient",
"type": "dotnet",
"request": "launch",
//"projectPath": "${workspaceFolder}/src/Yavsc/Yavsc.csproj",
"projectPath": "${workspaceFolder}/src/sampleWebAsWebApiClient/sampleWebAsWebApiClient.csproj",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
}
},
{
"name": "web",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/src/Yavsc/Yavsc.csproj",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
}
}
]
}

21
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "yavscdev",
"group": "yavsc",
"database": "YavscDev",
"username": "yavscdev",
"password": "admin"
}
],
"cSpell.words": [
"appsettings",
"Newtonsoft",
"Npgsql",
"Yavsc"
]
}

96
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,96 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-web",
"type": "process",
"problemMatcher": ["$msCompile"],
"command": "dotnet",
"args": [
"build"
],
"options": {
"cwd": "src/Yavsc"
},
"group": {
"kind": "build"
},
"isBackground": true
},
{
"label": "build-webapi",
"type": "process",
"problemMatcher": ["$msCompile"],
"command": "dotnet",
"args": [
"build"
],
"options": {
"cwd": "src/Api"
},
"group": {
"kind": "build"
},
"isBackground": true
},
{
"label": "build-webclient",
"type": "process",
"problemMatcher": ["$msCompile"],
"command": "dotnet",
"args": [
"build"
],
"runOptions": {
},
"options": {
"cwd": "src/sampleWebAsWebApiClient"
},
"group": {
"kind": "build"
},
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
}
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -8,29 +8,40 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MimeKit; using MimeKit;
using Yavsc.Abstract.Manage; using Yavsc.Abstract.Manage;
using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Identity;
using Yavsc.Interface; using Yavsc.Interface;
using Yavsc.Settings; using Yavsc.Settings;
using Yavsc.Models;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Localization;
using System.Web;
namespace Yavsc.Services namespace Yavsc.Services
{ {
public class MailSender : IEmailSender, ITrueEmailSender public class MailSender : IEmailSender<ApplicationUser>, IEmailSender, ITrueEmailSender
{ {
private readonly IStringLocalizer<YavscLocalization> localizer;
readonly SiteSettings siteSettings; readonly SiteSettings siteSettings;
readonly SmtpSettings smtpSettings; readonly SmtpSettings smtpSettings;
private readonly ILogger logger; private readonly ILogger logger;
public MailSender( public MailSender(
IOptions<SiteSettings> sitesOptions, IOptions<SiteSettings> sitesOptions,
IOptions<SmtpSettings> smtpOptions, IOptions<SmtpSettings> smtpOptions,
ILoggerFactory loggerFactory ILoggerFactory loggerFactory,
IStringLocalizer<Yavsc.YavscLocalization> localizer
) )
{ {
this.localizer = localizer;
siteSettings = sitesOptions.Value; siteSettings = sitesOptions.Value;
smtpSettings = smtpOptions.Value; smtpSettings = smtpOptions.Value;
logger = loggerFactory.CreateLogger<MailSender>(); logger = loggerFactory.CreateLogger<MailSender>();
} }
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink)
{
throw new NotImplementedException();
}
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -40,7 +51,7 @@ namespace Yavsc.Services
/// <returns>a MessageWithPayloadResponse, /// <returns>a MessageWithPayloadResponse,
/// <c>bool somethingsent = (response.failure == 0 &amp;&amp; response.success > 0)</c> /// <c>bool somethingsent = (response.failure == 0 &amp;&amp; response.success > 0)</c>
/// </returns> /// </returns>
public async Task SendEmailAsync(string email, string subject, string htmlMessage) public async Task SendEmailAsync(string email, string subject, string htmlMessage)
{ {
@ -79,5 +90,25 @@ namespace Yavsc.Services
} }
return msg.MessageId; return msg.MessageId;
} }
public async Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode)
{
var callbackUrl = siteSettings.Audience + "/Account/ResetPassword/" +
HttpUtility.UrlEncode(user.Id) + "/" + HttpUtility.UrlEncode(resetCode);
await SendEmailAsync(user.UserName, user.Email,
localizer["Reset Password"],
localizer["Please reset your password by "] + " <a href=\"" +
callbackUrl + "\" >following this link</a>");
throw new NotImplementedException();
}
public async Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink)
{
await SendEmailAsync(user.UserName, user.Email,
localizer["Reset Password"],
localizer["Please reset your password by "] + " <a href=\"" +
resetLink + "\" >following this link</a>");
}
} }
} }

View File

@ -2,15 +2,12 @@ using System;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor;
using Microsoft.Extensions.PlatformAbstractions;
using Yavsc; using Yavsc;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Services; using Yavsc.Services;
using Microsoft.Data.Entity;
using Microsoft.Extensions.WebEncoders; using Microsoft.Extensions.WebEncoders;
using yavscTests.Settings; using yavscTests.Settings;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
@ -18,7 +15,6 @@ using System.Net;
using Yavsc.Extensions; using Yavsc.Extensions;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using OAuth.AspNet.Tokens;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OAuth;
@ -27,7 +23,6 @@ using Google.Apis.Util.Store;
using System.Security.Claims; using System.Security.Claims;
using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Auth.OAuth2.Responses;
using Constants = Yavsc.Constants; using Constants = Yavsc.Constants;
using OAuth.AspNet.AuthServer;
using Yavsc.Models.Auth; using Yavsc.Models.Auth;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@ -39,15 +34,14 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Yavsc.AuthorizationHandlers;
using Yavsc.Formatters;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using static Yavsc.Startup; using static Yavsc.Startup;
using Microsoft.AspNetCore.DataProtection.Infrastructure; using Microsoft.AspNetCore.DataProtection.Infrastructure;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Identity.EntityFramework;
using Yavsc.Auth;
using Yavsc.Lib; using Yavsc.Lib;
using Yavsc.Settings;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Authorization;
namespace yavscTests namespace yavscTests
{ {
@ -65,10 +59,7 @@ namespace yavscTests
public static IConfigurationRoot GoogleWebClientConfiguration { get; set; } public static IConfigurationRoot GoogleWebClientConfiguration { get; set; }
public static CookieAuthenticationOptions ExternalCookieAppOptions { get; private set; } public static CookieAuthenticationOptions ExternalCookieAppOptions { get; private set; }
public MonoDataProtectionProvider ProtectionProvider { get; private set; } private static ILogger logger;
public static OAuth.AspNet.AuthServer.OAuthAuthorizationServerOptions OAuthServerAppOptions { get; private set; }
public Yavsc.Auth.YavscGoogleOptions YavscGoogleAppOptions { get; private set; }
private static ILogger logger;
public static string ApiKey { get; private set; } public static string ApiKey { get; private set; }
@ -112,10 +103,8 @@ namespace yavscTests
var testingconf = Configuration.GetSection("Testing"); var testingconf = Configuration.GetSection("Testing");
services.Configure<TestingSetup>(testingconf); services.Configure<TestingSetup>(testingconf);
services.AddInstance(typeof(ILoggerFactory), new LoggerFactory()); services.AddTransient(typeof(IEmailSender), typeof(MailSender));
services.AddTransient(typeof(IEmailSender), typeof(MailSender));
services.AddEntityFramework().AddNpgsql().AddDbContext<ApplicationDbContext>(); services.AddEntityFramework().AddNpgsql().AddDbContext<ApplicationDbContext>();
services.AddTransient((s) => new RazorTemplateEngine(s.GetService<RazorEngineHost>()));
services.AddLogging(); services.AddLogging();
services.AddTransient<ServerSideFixture>(); services.AddTransient<ServerSideFixture>();
services.AddTransient<MailSender>(); services.AddTransient<MailSender>();
@ -135,22 +124,16 @@ namespace yavscTests
// Add the system clock service // Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>(); services.AddSingleton<ISystemClock, SystemClock>();
services.AddAuthorization(options => services.AddAuthorizationBuilder()
{ .AddPolicy("AdministratorOnly", policy =>
options.AddPolicy("AdministratorOnly", policy =>
{ {
policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName); policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName);
}); })
.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName))
options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); .AddPolicy("Bearer", new AuthorizationPolicyBuilder()
options.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("yavsc") .AddAuthenticationSchemes("yavsc")
.RequireAuthenticatedUser().Build()); .RequireAuthenticatedUser().Build())
// options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); .AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
// options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement()));
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
});
services.AddDataProtection(); services.AddDataProtection();
services.ConfigureDataProtection(configure => services.ConfigureDataProtection(configure =>
@ -181,9 +164,7 @@ namespace yavscTests
services.AddIdentity<ApplicationUser, IdentityRole>( services.AddIdentity<ApplicationUser, IdentityRole>(
option => option =>
{ {
IdentityAppOptions = option;
option.User.RequireUniqueEmail = true; option.User.RequireUniqueEmail = true;
option.Cookies.ApplicationCookie.LoginPath = "/signin";
} }
).AddEntityFrameworkStores<ApplicationDbContext>() ).AddEntityFrameworkStores<ApplicationDbContext>()
.AddTokenProvider<EmailTokenProvider<ApplicationUser>>(Constants.DefaultFactor) .AddTokenProvider<EmailTokenProvider<ApplicationUser>>(Constants.DefaultFactor)
@ -221,22 +202,18 @@ namespace yavscTests
// Inject ticket formatting // Inject ticket formatting
services.AddTransient(typeof(ISecureDataFormat<>), typeof(SecureDataFormat<>)); services.AddTransient(typeof(ISecureDataFormat<>), typeof(SecureDataFormat<>));
services.AddTransient<Microsoft.AspNet.Authentication.ISecureDataFormat<AuthenticationTicket>, Microsoft.AspNet.Authentication.SecureDataFormat<AuthenticationTicket>>();
services.AddTransient<ISecureDataFormat<AuthenticationTicket>, TicketDataFormat>(); services.AddTransient<ISecureDataFormat<AuthenticationTicket>, TicketDataFormat>();
// Add application services. // Add application services.
services.AddTransient<IEmailSender, MailSender>(); services.AddTransient<IEmailSender<ApplicationUser>, MailSender>();
services.AddTransient<IYavscMessageSender, YavscMessageSender>(); services.AddTransient<IYavscMessageSender, YavscMessageSender>();
services.AddTransient<IBillingService, BillingService>(); services.AddTransient<IBillingService, BillingService>();
services.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false)); services.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false));
services.AddTransient<ICalendarManager, CalendarManager>(); services.AddTransient<ICalendarManager, CalendarManager>();
services.AddTransient<Microsoft.Extensions.WebEncoders.UrlEncoder, UrlEncoder>();
} }
#region OAuth Methods
private Client GetApplication(string clientId) private Client GetApplication(string clientId)
{ {
@ -250,159 +227,6 @@ namespace yavscTests
return app; return app;
} }
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context == null) throw new InvalidOperationException("context == null");
var app = GetApplication(context.ClientId);
if (app == null) return Task.FromResult(0);
Startup.logger.LogInformation($"ValidateClientRedirectUri: Validated ({app.RedirectUri})");
context.Validated(app.RedirectUri);
return Task.FromResult(0);
}
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId, clientSecret;
if (context.TryGetBasicCredentials(out clientId, out clientSecret) ||
context.TryGetFormCredentials(out clientId, out clientSecret))
{
logger.LogInformation($"ValidateClientAuthentication: Got id: ({clientId} secret: {clientSecret})");
var client = GetApplication(clientId);
if (client == null)
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
else
if (client.Type == ApplicationTypes.NativeConfidential)
{
logger.LogInformation($"NativeConfidential key");
if (string.IsNullOrWhiteSpace(clientSecret))
{
logger.LogInformation($"invalid_clientId: Client secret should be sent.");
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
// if (client.Secret != Helper.GetHash(clientSecret))
// TODO store a hash in db, not the pass
if (client.Secret != clientSecret)
{
context.SetError("invalid_clientId", "Client secret is invalid.");
logger.LogInformation($"invalid_clientId: Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
}
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
logger.LogInformation($"invalid_clientId: Client is inactive.");
return Task.FromResult<object>(null);
}
if (client != null && client.Secret == clientSecret)
{
logger.LogInformation($"\\o/ ValidateClientAuthentication: Validated ({clientId})");
context.Validated();
}
else logger.LogInformation($":'( ValidateClientAuthentication: KO ({clientId})");
}
else logger.LogWarning($"ValidateClientAuthentication: neither Basic nor Form credential were found");
return Task.FromResult(0);
}
private async Task<Task> GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
logger.LogWarning($"GrantResourceOwnerCredentials task ... {context.UserName}");
ApplicationUser user = null;
user = DbContext.Users.Include(u => u.Membership).First(u => u.UserName == context.UserName);
if (await _usermanager.CheckPasswordAsync(user, context.Password))
{
var claims = new List<Claim>(
context.Scope.Select(x => new Claim("urn:oauth:scope", x))
)
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email)
};
claims.AddRange((await _usermanager.GetRolesAsync(user)).Select(
r => new Claim(ClaimTypes.Role, r)
));
claims.AddRange(user.Membership.Select(
m => new Claim(YavscClaimTypes.CircleMembership, m.CircleId.ToString())
));
ClaimsPrincipal principal = new ClaimsPrincipal(
new ClaimsIdentity(
new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType),
claims)
);
context.HttpContext.User = principal;
context.Validated(principal);
}
#if USERMANAGER
#endif
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var id = new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType);
var claims = context.Scope.Select(x => new Claim("urn:oauth:scope", x));
var cid = new ClaimsIdentity(id, claims);
ClaimsPrincipal principal = new ClaimsPrincipal(cid);
context.Validated(principal);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
logger.LogInformation("CreateAuthenticationCode");
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
logger.LogInformation("ReceiveAuthenticationCode: Success");
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
var uid = context.Ticket.Principal.GetUserId();
logger.LogInformation($"CreateRefreshToken for {uid}");
foreach (var c in context.Ticket.Principal.Claims)
logger.LogInformation($"| User claim: {c.Type} {c.Value}");
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
var uid = context.Ticket.Principal.GetUserId();
logger.LogInformation($"ReceiveRefreshToken for {uid}");
foreach (var c in context.Ticket.Principal.Claims)
logger.LogInformation($"| User claim: {c.Type} {c.Value}");
context.DeserializeTicket(context.Token);
}
#endregion
UserManager<ApplicationUser> _usermanager; UserManager<ApplicationUser> _usermanager;
@ -415,8 +239,6 @@ namespace yavscTests
ILoggerFactory loggerFactory ILoggerFactory loggerFactory
) )
{ {
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
logger = loggerFactory.CreateLogger<Startup>(); logger = loggerFactory.CreateLogger<Startup>();
logger.LogInformation(env.EnvironmentName); logger.LogInformation(env.EnvironmentName);
this.DbContext = dbContext; this.DbContext = dbContext;

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Yavsc\Yavsc.csproj" />
<ProjectReference Include="..\..\src\Yavsc.Abstract\Yavsc.Abstract.csproj" />
<ProjectReference Include="..\..\src\Yavsc.Server\Yavsc.Server.csproj" />
</ItemGroup>
</Project>