✨ Account and auth protobuf and client code
This commit is contained in:
		
							
								
								
									
										22
									
								
								DysonNetwork.Shared/DysonNetwork.Shared.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								DysonNetwork.Shared/DysonNetwork.Shared.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|  |     <PropertyGroup> | ||||||
|  |         <TargetFramework>net9.0</TargetFramework> | ||||||
|  |         <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |         <Nullable>enable</Nullable> | ||||||
|  |     </PropertyGroup> | ||||||
|  |  | ||||||
|  |     <ItemGroup> | ||||||
|  |         <Protobuf Include="Protos\*.proto" GrpcServices="Client" /> | ||||||
|  |     </ItemGroup> | ||||||
|  |  | ||||||
|  |     <ItemGroup> | ||||||
|  |         <PackageReference Include="Grpc.Net.Client" Version="2.65.0" /> | ||||||
|  |         <PackageReference Include="Google.Protobuf" Version="3.27.2" /> | ||||||
|  |         <PackageReference Include="Grpc.Tools" Version="2.65.0"> | ||||||
|  |             <PrivateAssets>all</PrivateAssets> | ||||||
|  |             <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||||
|  |         </PackageReference> | ||||||
|  |     </ItemGroup> | ||||||
|  |  | ||||||
|  | </Project> | ||||||
							
								
								
									
										28
									
								
								DysonNetwork.Shared/Protos/account.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Shared/Protos/account.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package dyson_network.sphere.account; | ||||||
|  |  | ||||||
|  | import "google/protobuf/empty.proto"; | ||||||
|  |  | ||||||
|  | option csharp_namespace = "DysonNetwork.Sphere.Account.Proto"; | ||||||
|  |  | ||||||
|  | service AccountService { | ||||||
|  |   // Retrieves the current user's account information | ||||||
|  |   rpc GetAccount(google.protobuf.Empty) returns (AccountResponse); | ||||||
|  |  | ||||||
|  |   // Updates the current user's account information | ||||||
|  |   rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message AccountResponse { | ||||||
|  |   string id = 1; | ||||||
|  |   string username = 2; | ||||||
|  |   string email = 3; | ||||||
|  |   string display_name = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message UpdateAccountRequest { | ||||||
|  |   // Fields to update | ||||||
|  |   optional string email = 1; | ||||||
|  |   optional string display_name = 2; | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								DysonNetwork.Shared/Protos/auth.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								DysonNetwork.Shared/Protos/auth.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package dyson_network.sphere.auth; | ||||||
|  |  | ||||||
|  | import "google/protobuf/empty.proto"; | ||||||
|  | import "google/protobuf/timestamp.proto"; | ||||||
|  |  | ||||||
|  | option csharp_namespace = "DysonNetwork.Sphere.Auth.Proto"; | ||||||
|  |  | ||||||
|  | service AuthService { | ||||||
|  |   // Standard username/password login | ||||||
|  |   rpc Login(LoginRequest) returns (LoginResponse); | ||||||
|  |  | ||||||
|  |   // Introspects an OAuth 2.0 access token. | ||||||
|  |   rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse); | ||||||
|  |  | ||||||
|  |   // Logs out the current session | ||||||
|  |   rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message LoginRequest { | ||||||
|  |   string username = 1; | ||||||
|  |   string password = 2; | ||||||
|  |   // Optional: for 2FA | ||||||
|  |   string two_factor_code = 3; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message LoginResponse { | ||||||
|  |   string access_token = 1; | ||||||
|  |   string refresh_token = 2; | ||||||
|  |   int64 expires_in = 3; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message IntrospectTokenRequest { | ||||||
|  |   string token = 1; | ||||||
|  |   // Optional: token_type_hint can be "access_token" or "refresh_token" | ||||||
|  |   string token_type_hint = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message IntrospectionResponse { | ||||||
|  |   // Indicates whether or not the token is currently active. | ||||||
|  |   bool active = 1; | ||||||
|  |   // A JSON string containing the claims of the token. | ||||||
|  |   string claims = 2; | ||||||
|  |   // The client identifier for the OAuth 2.0 client that requested the token. | ||||||
|  |   string client_id = 3; | ||||||
|  |   // The username of the resource owner who authorized the token. | ||||||
|  |   string username = 4; | ||||||
|  |   // The scope of the access token. | ||||||
|  |   string scope = 5; | ||||||
|  |   // The time at which the token was issued. | ||||||
|  |   google.protobuf.Timestamp iat = 6; | ||||||
|  |   // The time at which the token expires. | ||||||
|  |   google.protobuf.Timestamp exp = 7; | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								DysonNetwork.Sphere/Account/AccountGrpcService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								DysonNetwork.Sphere/Account/AccountGrpcService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | using DysonNetwork.Sphere.Account.Proto; | ||||||
|  | using Grpc.Core; | ||||||
|  | using Google.Protobuf.WellKnownTypes; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using DysonNetwork.Sphere.Auth; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Sphere.Account; | ||||||
|  |  | ||||||
|  | public class AccountGrpcService : DysonNetwork.Sphere.Account.Proto.AccountService.AccountServiceBase | ||||||
|  | { | ||||||
|  |     private readonly AppDatabase _db; | ||||||
|  |     private readonly AuthService _auth; | ||||||
|  |  | ||||||
|  |     public AccountGrpcService(AppDatabase db, AuthService auth) | ||||||
|  |     { | ||||||
|  |         _db = db; | ||||||
|  |         _auth = auth; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<AccountResponse> GetAccount(Empty request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var account = await GetAccountFromContext(context); | ||||||
|  |         return ToAccountResponse(account); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<AccountResponse> UpdateAccount(UpdateAccountRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var account = await GetAccountFromContext(context); | ||||||
|  |  | ||||||
|  |         if (request.Email != null) | ||||||
|  |         { | ||||||
|  |             var emailContact = await _db.AccountContacts.FirstOrDefaultAsync(c => c.AccountId == account.Id && c.Type == AccountContactType.Email); | ||||||
|  |             if (emailContact != null) | ||||||
|  |             { | ||||||
|  |                 emailContact.Content = request.Email; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                 account.Contacts.Add(new AccountContact { Type = AccountContactType.Email, Content = request.Email }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (request.DisplayName != null) | ||||||
|  |         { | ||||||
|  |             account.Nick = request.DisplayName; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await _db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         return ToAccountResponse(account); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private async Task<DysonNetwork.Sphere.Account.Account> GetAccountFromContext(ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization"); | ||||||
|  |         if (authorizationHeader == null) | ||||||
|  |         { | ||||||
|  |             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Missing authorization header.")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var token = authorizationHeader.Value.Replace("Bearer ", ""); | ||||||
|  |         if (!_auth.ValidateToken(token, out var sessionId)) | ||||||
|  |         { | ||||||
|  |             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid token.")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var session = await _db.AuthSessions.Include(s => s.Account).ThenInclude(a => a.Contacts).FirstOrDefaultAsync(s => s.Id == sessionId); | ||||||
|  |         if (session == null) | ||||||
|  |         { | ||||||
|  |             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Session not found.")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return session.Account; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private AccountResponse ToAccountResponse(DysonNetwork.Sphere.Account.Account account) | ||||||
|  |     { | ||||||
|  |         var emailContact = account.Contacts.FirstOrDefault(c => c.Type == AccountContactType.Email); | ||||||
|  |         return new AccountResponse | ||||||
|  |         { | ||||||
|  |             Id = account.Id.ToString(), | ||||||
|  |             Username = account.Name, | ||||||
|  |             Email = emailContact?.Content ?? "", | ||||||
|  |             DisplayName = account.Nick | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								DysonNetwork.Sphere/Auth/AuthGrpcService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								DysonNetwork.Sphere/Auth/AuthGrpcService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | using DysonNetwork.Sphere.Auth.Proto; | ||||||
|  | using Grpc.Core; | ||||||
|  | using Google.Protobuf.WellKnownTypes; | ||||||
|  | using DysonNetwork.Sphere.Account; | ||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using NodaTime; | ||||||
|  | using System.Text.Json; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Sphere.Auth; | ||||||
|  |  | ||||||
|  | public class AuthGrpcService : DysonNetwork.Sphere.Auth.Proto.AuthService.AuthServiceBase | ||||||
|  | { | ||||||
|  |     private readonly AppDatabase _db; | ||||||
|  |     private readonly AccountService _accounts; | ||||||
|  |     private readonly AuthService _auth; | ||||||
|  |  | ||||||
|  |     public AuthGrpcService(AppDatabase db, AccountService accounts, AuthService auth) | ||||||
|  |     { | ||||||
|  |         _db = db; | ||||||
|  |         _accounts = accounts; | ||||||
|  |         _auth = auth; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<LoginResponse> Login(LoginRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var account = await _accounts.LookupAccount(request.Username); | ||||||
|  |         if (account == null) | ||||||
|  |         { | ||||||
|  |             throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, "Account not found.")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var factor = await _db.AccountAuthFactors.FirstOrDefaultAsync(f => f.AccountId == account.Id && f.Type == AccountAuthFactorType.Password); | ||||||
|  |         if (factor == null || !factor.VerifyPassword(request.Password)) | ||||||
|  |         { | ||||||
|  |             throw new RpcException(new Grpc.Core.Status(StatusCode.Unauthenticated, "Invalid credentials.")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var session = new Session | ||||||
|  |         { | ||||||
|  |             LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), | ||||||
|  |             ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), | ||||||
|  |             Account = account, | ||||||
|  |             Challenge = new Challenge() // Create a dummy challenge | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         _db.AuthSessions.Add(session); | ||||||
|  |         await _db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         var token = _auth.CreateToken(session); | ||||||
|  |  | ||||||
|  |         return new LoginResponse | ||||||
|  |         { | ||||||
|  |             AccessToken = token, | ||||||
|  |             ExpiresIn = (long)(session.ExpiredAt.Value - session.LastGrantedAt.Value).TotalSeconds | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<IntrospectionResponse> IntrospectToken(IntrospectTokenRequest request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         if (_auth.ValidateToken(request.Token, out var sessionId)) | ||||||
|  |         { | ||||||
|  |             var session = await _db.AuthSessions | ||||||
|  |                 .Include(s => s.Account) | ||||||
|  |                 .Include(s => s.Challenge) | ||||||
|  |                 .FirstOrDefaultAsync(s => s.Id == sessionId); | ||||||
|  |  | ||||||
|  |             if (session != null) | ||||||
|  |             { | ||||||
|  |                 return new IntrospectionResponse | ||||||
|  |                 { | ||||||
|  |                     Active = true, | ||||||
|  |                     Claims = JsonSerializer.Serialize(new { sub = session.AccountId }), | ||||||
|  |                     ClientId = session.AppId?.ToString() ?? "", | ||||||
|  |                     Username = session.Account.Name, | ||||||
|  |                     Scope = string.Join(" ", session.Challenge.Scopes), | ||||||
|  |                     Iat = Timestamp.FromDateTime(session.CreatedAt.ToDateTimeUtc()), | ||||||
|  |                     Exp = Timestamp.FromDateTime(session.ExpiredAt?.ToDateTimeUtc() ?? DateTime.MaxValue) | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return new IntrospectionResponse { Active = false }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override async Task<Empty> Logout(Empty request, ServerCallContext context) | ||||||
|  |     { | ||||||
|  |         var authorizationHeader = context.RequestHeaders.FirstOrDefault(h => h.Key == "authorization"); | ||||||
|  |         if (authorizationHeader != null) | ||||||
|  |         { | ||||||
|  |             var token = authorizationHeader.Value.Replace("Bearer ", ""); | ||||||
|  |             if (_auth.ValidateToken(token, out var sessionId)) | ||||||
|  |             { | ||||||
|  |                 var session = await _db.AuthSessions.FindAsync(sessionId); | ||||||
|  |                 if (session != null) | ||||||
|  |                 { | ||||||
|  |                     _db.AuthSessions.Remove(session); | ||||||
|  |                     await _db.SaveChangesAsync(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return new Empty(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -164,4 +164,13 @@ | |||||||
|         <_ContentIncludedByDefault Remove="app\publish\package.json" /> |         <_ContentIncludedByDefault Remove="app\publish\package.json" /> | ||||||
|     </ItemGroup> |     </ItemGroup> | ||||||
|  |  | ||||||
|  |     <ItemGroup> | ||||||
|  |         <PackageReference Include="Grpc.AspNetCore" Version="2.65.0" /> | ||||||
|  |     </ItemGroup> | ||||||
|  |  | ||||||
|  |     <ItemGroup> | ||||||
|  |         <Protobuf Include="Protos\auth.proto" GrpcServices="Server" /> | ||||||
|  |         <Protobuf Include="Protos\account.proto" GrpcServices="Server" /> | ||||||
|  |     </ItemGroup> | ||||||
|  |  | ||||||
| </Project> | </Project> | ||||||
|   | |||||||
| @@ -17,6 +17,9 @@ builder.Services.AddAppRateLimiting(); | |||||||
| builder.Services.AddAppAuthentication(); | builder.Services.AddAppAuthentication(); | ||||||
| builder.Services.AddAppSwagger(); | builder.Services.AddAppSwagger(); | ||||||
|  |  | ||||||
|  | // Add gRPC services | ||||||
|  | builder.Services.AddGrpc(); | ||||||
|  |  | ||||||
| // Add file storage | // Add file storage | ||||||
| builder.Services.AddAppFileStorage(builder.Configuration); | builder.Services.AddAppFileStorage(builder.Configuration); | ||||||
|  |  | ||||||
| @@ -44,4 +47,8 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>(); | |||||||
| // Configure application middleware pipeline | // Configure application middleware pipeline | ||||||
| app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); | app.ConfigureAppMiddleware(builder.Configuration, tusDiskStore); | ||||||
|  |  | ||||||
|  | // Map gRPC services | ||||||
|  | app.MapGrpcService<DysonNetwork.Sphere.Auth.AuthGrpcService>(); | ||||||
|  | app.MapGrpcService<DysonNetwork.Sphere.Account.AccountGrpcService>(); | ||||||
|  |  | ||||||
| app.Run(); | app.Run(); | ||||||
							
								
								
									
										28
									
								
								DysonNetwork.Sphere/Protos/account.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								DysonNetwork.Sphere/Protos/account.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package dyson_network.sphere.account; | ||||||
|  |  | ||||||
|  | import "google/protobuf/empty.proto"; | ||||||
|  |  | ||||||
|  | option csharp_namespace = "DysonNetwork.Sphere.Account.Proto"; | ||||||
|  |  | ||||||
|  | service AccountService { | ||||||
|  |   // Retrieves the current user's account information | ||||||
|  |   rpc GetAccount(google.protobuf.Empty) returns (AccountResponse); | ||||||
|  |  | ||||||
|  |   // Updates the current user's account information | ||||||
|  |   rpc UpdateAccount(UpdateAccountRequest) returns (AccountResponse); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message AccountResponse { | ||||||
|  |   string id = 1; | ||||||
|  |   string username = 2; | ||||||
|  |   string email = 3; | ||||||
|  |   string display_name = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message UpdateAccountRequest { | ||||||
|  |   // Fields to update | ||||||
|  |   optional string email = 1; | ||||||
|  |   optional string display_name = 2; | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								DysonNetwork.Sphere/Protos/auth.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								DysonNetwork.Sphere/Protos/auth.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | syntax = "proto3"; | ||||||
|  |  | ||||||
|  | package dyson_network.sphere.auth; | ||||||
|  |  | ||||||
|  | import "google/protobuf/empty.proto"; | ||||||
|  | import "google/protobuf/timestamp.proto"; | ||||||
|  |  | ||||||
|  | option csharp_namespace = "DysonNetwork.Sphere.Auth.Proto"; | ||||||
|  |  | ||||||
|  | service AuthService { | ||||||
|  |   // Standard username/password login | ||||||
|  |   rpc Login(LoginRequest) returns (LoginResponse); | ||||||
|  |  | ||||||
|  |   // Introspects an OAuth 2.0 access token. | ||||||
|  |   rpc IntrospectToken(IntrospectTokenRequest) returns (IntrospectionResponse); | ||||||
|  |  | ||||||
|  |   // Logs out the current session | ||||||
|  |   rpc Logout(google.protobuf.Empty) returns (google.protobuf.Empty); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message LoginRequest { | ||||||
|  |   string username = 1; | ||||||
|  |   string password = 2; | ||||||
|  |   // Optional: for 2FA | ||||||
|  |   string two_factor_code = 3; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message LoginResponse { | ||||||
|  |   string access_token = 1; | ||||||
|  |   string refresh_token = 2; | ||||||
|  |   int64 expires_in = 3; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message IntrospectTokenRequest { | ||||||
|  |   string token = 1; | ||||||
|  |   // Optional: token_type_hint can be "access_token" or "refresh_token" | ||||||
|  |   string token_type_hint = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message IntrospectionResponse { | ||||||
|  |   // Indicates whether or not the token is currently active. | ||||||
|  |   bool active = 1; | ||||||
|  |   // A JSON string containing the claims of the token. | ||||||
|  |   string claims = 2; | ||||||
|  |   // The client identifier for the OAuth 2.0 client that requested the token. | ||||||
|  |   string client_id = 3; | ||||||
|  |   // The username of the resource owner who authorized the token. | ||||||
|  |   string username = 4; | ||||||
|  |   // The scope of the access token. | ||||||
|  |   string scope = 5; | ||||||
|  |   // The time at which the token was issued. | ||||||
|  |   google.protobuf.Timestamp iat = 6; | ||||||
|  |   // The time at which the token expires. | ||||||
|  |   google.protobuf.Timestamp exp = 7; | ||||||
|  | } | ||||||
| @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution | |||||||
| 		compose.yaml = compose.yaml | 		compose.yaml = compose.yaml | ||||||
| 	EndProjectSection | 	EndProjectSection | ||||||
| EndProject | EndProject | ||||||
|  | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Shared", "DysonNetwork.Shared\DysonNetwork.Shared.csproj", "{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}" | ||||||
|  | EndProject | ||||||
| Global | Global | ||||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||||
| 		Debug|Any CPU = Debug|Any CPU | 		Debug|Any CPU = Debug|Any CPU | ||||||
| @@ -17,5 +19,9 @@ Global | |||||||
| 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Debug|Any CPU.Build.0 = Debug|Any CPU | 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
| 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.ActiveCfg = Release|Any CPU | 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
| 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.Build.0 = Release|Any CPU | 		{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
|  | 		{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
|  | 		{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
|  | 		{C24C6C96-2D99-4D0C-950B-4C3D8B55E930}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
| 	EndGlobalSection | 	EndGlobalSection | ||||||
| EndGlobal | EndGlobal | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user