diff --git a/Gateway/Gateway.Api/Gateway.Api.csproj b/Gateway/Gateway.Api/Gateway.Api.csproj
index 562aa36..14af26c 100644
--- a/Gateway/Gateway.Api/Gateway.Api.csproj
+++ b/Gateway/Gateway.Api/Gateway.Api.csproj
@@ -7,11 +7,21 @@
bc1b7e89-5cda-4112-8b7c-ff0162e59ce3
Linux
..\..
+ true
-
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Gateway/Gateway.Api/Program.cs b/Gateway/Gateway.Api/Program.cs
index 61abfb6..7a4767c 100644
--- a/Gateway/Gateway.Api/Program.cs
+++ b/Gateway/Gateway.Api/Program.cs
@@ -1,12 +1,62 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+using System.Text;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddSwaggerGen();
+byte[] key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key is missing."));
+
+builder.Services.AddAuthentication(cfg =>
+{
+ cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+}).AddJwtBearer(options =>
+{
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = builder.Configuration["Jwt:Issuer"],
+ ValidAudience = builder.Configuration["Jwt:Audience"],
+ IssuerSigningKey = new SymmetricSecurityKey(key),
+ };
+});
+
+builder.Services.AddAuthorization(options =>
+{
+ options.AddPolicy("Bearer", policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
+ });
+});
+
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
+builder.Services.AddCors(
+ options =>
+ {
+ options.AddPolicy(
+ "CorsPolicy",
+ policyBuilder => policyBuilder
+ .AllowAnyMethod()
+ .AllowCredentials()
+ .SetIsOriginAllowed((_) => true)
+ .AllowAnyHeader());
+ });
var app = builder.Build();
+app.UseCors("CorsPolicy");
+
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
@@ -16,6 +66,8 @@
app.UseHttpsRedirection();
+app.UseAuthentication();
+app.UseAuthorization();
app.MapReverseProxy();
app.Run();
diff --git a/Gateway/Gateway.Api/appsettings.json b/Gateway/Gateway.Api/appsettings.json
index d54d8ac..8e7e0f6 100644
--- a/Gateway/Gateway.Api/appsettings.json
+++ b/Gateway/Gateway.Api/appsettings.json
@@ -6,16 +6,31 @@
}
},
"AllowedHosts": "*",
+ "Jwt": {
+ "Key": "YourSuperLongSecretKeyThatIsAtLeast32Characters!",
+ "Issuer": "AuthService",
+ "Audience": "Gateway"
+ },
"ReverseProxy": {
"Routes": {
"CoreService": {
"ClusterId": "coreServiceCluster",
+ "AuthorizationPolicy": "Bearer",
"Match": { "Path": "/core-api/{**catch-all}" },
"Transforms": [
{
"PathPattern": "/api/{**catch-all}"
}
]
+ },
+ "AuthService": {
+ "ClusterId": "authServiceCluster",
+ "Match": { "Path": "/auth-api/{**catch-all}" },
+ "Transforms": [
+ {
+ "PathPattern": "/{**catch-all}"
+ }
+ ]
}
},
"Clusters": {
@@ -23,6 +38,11 @@
"Destinations": {
"CoreService": { "Address": "http://core.api:8080/" }
}
+ },
+ "authServiceCluster": {
+ "Destinations": {
+ "AuthService": { "Address": "http://auth.api:8080/" }
+ }
}
}
}
diff --git a/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs b/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs
new file mode 100644
index 0000000..b7ffb96
--- /dev/null
+++ b/Helpers/GatewayAuthHandler/GatewayAuthHandler.cs
@@ -0,0 +1,58 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+namespace GatewayAuthHandler
+{
+ using System.IdentityModel.Tokens.Jwt;
+ using System.Security.Claims;
+ using System.Text.Encodings.Web;
+ using Microsoft.AspNetCore.Authentication;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Options;
+
+ ///
+ /// Gateway authentication handler.
+ ///
+ public class GatewayAuthHandler : AuthenticationHandler
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Authenitcation scheme options.
+ /// Logger.
+ /// Encoder.
+ /// System clock.
+ public GatewayAuthHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock systemClock)
+ : base(options, logger, encoder, systemClock)
+ {
+ }
+
+ ///
+ /// Handle authenticvation method.
+ ///
+ /// Authentication result.
+ protected override Task HandleAuthenticateAsync()
+ {
+ if (!this.Request.Headers.TryGetValue("Authorization", out var authHeader))
+ {
+ return Task.FromResult(AuthenticateResult.Fail("No Authorization Header"));
+ }
+
+ var token = authHeader.ToString().Replace("Bearer ", string.Empty, StringComparison.OrdinalIgnoreCase);
+ var handler = new JwtSecurityTokenHandler();
+ var jwtToken = handler.ReadJwtToken(token);
+
+ var claims = jwtToken.Claims.ToList();
+ var identity = new ClaimsIdentity(claims, "GatewayAuth");
+ var principal = new ClaimsPrincipal(identity);
+ var ticket = new AuthenticationTicket(principal, "GatewayAuth");
+
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+ }
+}
diff --git a/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj b/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj
new file mode 100644
index 0000000..c5e013c
--- /dev/null
+++ b/Helpers/GatewayAuthHandler/GatewayAuthHandler.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/PracticesService.sln b/PracticesService.sln
index ca5b7b5..83c7bc4 100644
--- a/PracticesService.sln
+++ b/PracticesService.sln
@@ -13,6 +13,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gateway", "Gateway", "{222D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gateway.Api", "Gateway\Gateway.Api\Gateway.Api.csproj", "{C5116E8B-8D09-406D-80CD-0039B4AC5B68}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthService.Api", "Services\AuthService.Api\AuthService.Api.csproj", "{924ED870-509D-4512-A12A-1DC2E81A6E41}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helpers", "Helpers", "{8CD65409-E7E2-4FC3-8AA5-3BFA5D08779B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatewayAuthHandler", "Helpers\GatewayAuthHandler\GatewayAuthHandler.csproj", "{48704C05-2136-429D-A249-628CDEF16B0D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{1665257F-51D8-4832-A7ED-94603EA35B23}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared\Shared.csproj", "{712BF23F-5D58-4EB5-94F5-8525ED05965D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{39D921BF-8448-4A15-A870-778AEFA3EF2B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests\Tests.csproj", "{76C651ED-813E-4E68-8C0D-C6D7C354B29C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +45,22 @@ Global
{C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5116E8B-8D09-406D-80CD-0039B4AC5B68}.Release|Any CPU.Build.0 = Release|Any CPU
+ {924ED870-509D-4512-A12A-1DC2E81A6E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {924ED870-509D-4512-A12A-1DC2E81A6E41}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {924ED870-509D-4512-A12A-1DC2E81A6E41}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {924ED870-509D-4512-A12A-1DC2E81A6E41}.Release|Any CPU.Build.0 = Release|Any CPU
+ {48704C05-2136-429D-A249-628CDEF16B0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48704C05-2136-429D-A249-628CDEF16B0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48704C05-2136-429D-A249-628CDEF16B0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48704C05-2136-429D-A249-628CDEF16B0D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {712BF23F-5D58-4EB5-94F5-8525ED05965D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {76C651ED-813E-4E68-8C0D-C6D7C354B29C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -38,6 +68,10 @@ Global
GlobalSection(NestedProjects) = preSolution
{94A409B2-9D11-41F4-9BB8-6B89AC26764E} = {89CC78B4-9A7B-46F4-B786-7FB0D49911B2}
{C5116E8B-8D09-406D-80CD-0039B4AC5B68} = {222D815F-4394-417D-AF31-DA86AE6E21C1}
+ {924ED870-509D-4512-A12A-1DC2E81A6E41} = {89CC78B4-9A7B-46F4-B786-7FB0D49911B2}
+ {48704C05-2136-429D-A249-628CDEF16B0D} = {8CD65409-E7E2-4FC3-8AA5-3BFA5D08779B}
+ {712BF23F-5D58-4EB5-94F5-8525ED05965D} = {1665257F-51D8-4832-A7ED-94603EA35B23}
+ {76C651ED-813E-4E68-8C0D-C6D7C354B29C} = {39D921BF-8448-4A15-A870-778AEFA3EF2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC6C6DEA-3EB6-439A-AE03-43698361909A}
diff --git a/README.md b/README.md
index d77eb71..0b6a5b5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,104 @@
# PracticesService
-Сервис для работы с учебными/производственными практиками кафедры
+
+## Описание
+
+**PracticesService** — Сервис для работы с учебными/производственными практиками кафедры. Запуск производится через Docker Compose.
+
+## Контейнеры
+
+- **RabbitMQ** — брокер сообщений
+- **gateway.api** — шлюзовый API
+- **core.api / core.db** — основной сервис и его база данных
+- **auth.api / auth.db** — сервис авторизации и его база данных
+- **frontend** — клиентская часть
+
+## Предварительные требования
+
+- [Docker](https://www.docker.com/)
+- [Docker Compose](https://docs.docker.com/compose/)
+
+## Запуск проекта
+
+1. Клонируйте репозиторий:
+
+ ```bash
+ git clone
+ cd <название директории>
+ ```
+
+2. Запустите все сервисы:
+
+ ```bash
+ docker-compose up --build
+ ```
+
+ Все сервисы поднимутся автоматически, включая RabbitMQ, базы данных и API.
+
+3. Убедитесь, что RabbitMQ доступен по адресу:
+ ```
+ http://localhost:15672
+ ```
+ Логин: `admin`, пароль: `admin123`
+
+## Применение миграций для AuthService
+
+После запуска контейнеров, необходимо применить миграции к базе данных авторизации.
+
+1. Войдите в контейнер `auth.api`:
+
+ ```bash
+ docker exec -it /bin/sh
+ ```
+
+ Например:
+
+ ```bash
+ docker exec -it auth.api /bin/sh
+ ```
+
+2. Примените миграции (предположим, используется Entity Framework CLI):
+
+ ```bash
+ dotnet ef database update --project Services/AuthService.Api
+ ```
+
+ > Убедитесь, что внутри контейнера установлен `dotnet-ef`. Если нет — установите или примените миграции локально и пересоберите образ.
+
+## Доступ к сервисам
+
+| Сервис | URL |
+|--------------|----------------------------|
+| Gateway API | http://localhost:5000 |
+| Core API | http://localhost:5001 |
+| Auth API | http://localhost:5002 |
+| Frontend | http://localhost:8000 |
+| RabbitMQ UI | http://localhost:15672 |
+
+## Сеть
+
+Все сервисы находятся в общей Docker-сети `proxybackend`.
+
+---
+
+## Полезные команды
+
+- Остановка всех сервисов:
+
+ ```bash
+ docker-compose down
+ ```
+
+- Просмотр логов:
+
+ ```bash
+ docker-compose logs -f
+ ```
+
+- Проверка состояния контейнеров:
+
+ ```bash
+ docker ps
+ ```
+
+
+
diff --git a/Services/AuthService.Api/AuthService.Api.csproj b/Services/AuthService.Api/AuthService.Api.csproj
new file mode 100644
index 0000000..92899e1
--- /dev/null
+++ b/Services/AuthService.Api/AuthService.Api.csproj
@@ -0,0 +1,47 @@
+
+
+
+ net9.0
+ enable
+ enable
+ bc7ad2eb-e1dc-45a0-8580-18b7e93dedfb
+ Linux
+ ..\..
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs b/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs
new file mode 100644
index 0000000..0415f4e
--- /dev/null
+++ b/Services/AuthService.Api/Consumers/UserWithRoleActionConsumer.cs
@@ -0,0 +1,147 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+namespace AuthService.Api.Consumers;
+
+using Contracts;
+using MassTransit;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+
+///
+/// Consumer for user with roles events.
+///
+public class UserWithRoleActionConsumer : IConsumer
+{
+ private readonly AuthDbContext context;
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Auth DB context.
+ /// Logger.
+ public UserWithRoleActionConsumer(
+ AuthDbContext context,
+ ILogger logger)
+ {
+ this.context = context;
+ this.logger = logger;
+ }
+
+ ///
+ /// Consumes event.
+ ///
+ /// Consume event context.
+ /// A representing the asynchronous operation.
+ public async Task Consume(ConsumeContext consumeContext)
+ {
+ try
+ {
+ switch (consumeContext.Message.Action)
+ {
+ case UserActionType.Delete:
+ await this.HandleRoleRemoval(consumeContext.Message.UserId, consumeContext.Message.Role);
+ break;
+
+ case UserActionType.Create:
+ await this.HandleRoleAssignment(consumeContext.Message.UserId, consumeContext.Message.Role);
+ break;
+
+ case UserActionType.Update:
+ await this.HandleUserUpdate(
+ consumeContext.Message.UserId,
+ consumeContext.Message.FirstName,
+ consumeContext.Message.LastName,
+ consumeContext.Message.MiddleName);
+ break;
+
+ default:
+ this.logger.LogWarning("Unhandled action type: {Action}", consumeContext.Message.Action);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error processing user role action for user {UserId}", consumeContext.Message.UserId);
+ throw;
+ }
+ }
+
+ private async Task HandleRoleAssignment(string userId, string roleName)
+ {
+ var role = await this.context.Roles
+ .FirstOrDefaultAsync(r => r.Name == roleName);
+
+ if (role == null)
+ {
+ this.logger.LogError("Role {RoleName} not found", roleName);
+ throw new InvalidOperationException($"Role {roleName} not found");
+ }
+
+ var existingAssignment = await this.context.UserRoles
+ .AnyAsync(ur => ur.UserId == userId && ur.RoleId == role.Id);
+
+ if (existingAssignment)
+ {
+ this.logger.LogWarning("User {UserId} already has role {RoleName}", userId, roleName);
+ return;
+ }
+
+ this.context.UserRoles.Add(new IdentityUserRole
+ {
+ UserId = userId,
+ RoleId = role.Id,
+ });
+
+ await this.context.SaveChangesAsync();
+ this.logger.LogInformation("Assigned role {RoleName} to user {UserId}", roleName, userId);
+ }
+
+ private async Task HandleRoleRemoval(string userId, string roleName)
+ {
+ var role = await this.context.Roles
+ .FirstOrDefaultAsync(r => r.Name == roleName);
+
+ if (role == null)
+ {
+ this.logger.LogError("Role {RoleName} not found", roleName);
+ throw new InvalidOperationException($"Role {roleName} not found");
+ }
+
+ var userRole = await this.context.UserRoles
+ .FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == role.Id);
+
+ if (userRole == null)
+ {
+ this.logger.LogWarning("User {UserId} doesn't have role {RoleName}", userId, roleName);
+ return;
+ }
+
+ this.context.UserRoles.Remove(userRole);
+ await this.context.SaveChangesAsync();
+ this.logger.LogInformation("Removed role {RoleName} from user {UserId}", roleName, userId);
+ }
+
+ private async Task HandleUserUpdate(
+ string userId,
+ string firstName,
+ string lastName,
+ string? middleName)
+ {
+ var user = await this.context.Users.FindAsync(userId);
+ if (user == null)
+ {
+ this.logger.LogError("User {UserId} not found", userId);
+ throw new InvalidOperationException($"User {userId} not found");
+ }
+
+ user.FirstName = firstName;
+ user.LastName = lastName;
+ user.MiddleName = middleName;
+
+ await this.context.SaveChangesAsync();
+ this.logger.LogInformation("Updated user details for {UserId}", userId);
+ }
+}
\ No newline at end of file
diff --git a/Services/AuthService.Api/Data/AuthDbContext.cs b/Services/AuthService.Api/Data/AuthDbContext.cs
new file mode 100644
index 0000000..f56b239
--- /dev/null
+++ b/Services/AuthService.Api/Data/AuthDbContext.cs
@@ -0,0 +1,37 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+using AuthService.Api.Models;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+///
+/// Authentication service Db context.
+///
+public class AuthDbContext : IdentityDbContext
+{
+ ///
+ /// Initializes a new instance of the class.
+ /// Auth Db Context Constructor.
+ ///
+ /// Db Context options.
+ public AuthDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ ///
+ /// Gets or sets refresh token table.
+ ///
+ public DbSet RefreshTokens { get; set; }
+
+ ///
+ /// On model creating method.
+ ///
+ /// Model builder.
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+ }
+}
\ No newline at end of file
diff --git a/Services/AuthService.Api/Dockerfile b/Services/AuthService.Api/Dockerfile
new file mode 100644
index 0000000..faa54fe
--- /dev/null
+++ b/Services/AuthService.Api/Dockerfile
@@ -0,0 +1,30 @@
+# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
+
+# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+
+# Этот этап используется для сборки проекта службы
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Services/AuthService.Api/AuthService.Api.csproj", "Services/AuthService.Api/"]
+RUN dotnet restore "./Services/AuthService.Api/AuthService.Api.csproj"
+COPY . .
+WORKDIR "/src/Services/AuthService.Api"
+RUN dotnet build "./AuthService.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./AuthService.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "AuthService.Api.dll"]
\ No newline at end of file
diff --git a/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs
new file mode 100644
index 0000000..532777e
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.Designer.cs
@@ -0,0 +1,276 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ [DbContext(typeof(AuthDbContext))]
+ [Migration("20250212205718_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs
new file mode 100644
index 0000000..ffcc3b1
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250212205718_InitialCreate.cs
@@ -0,0 +1,227 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ using System;
+ using Microsoft.EntityFrameworkCore.Migrations;
+ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(type: "text", nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "text", nullable: false),
+ UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(type: "boolean", nullable: false),
+ PasswordHash = table.Column(type: "text", nullable: true),
+ SecurityStamp = table.Column(type: "text", nullable: true),
+ ConcurrencyStamp = table.Column(type: "text", nullable: true),
+ PhoneNumber = table.Column(type: "text", nullable: true),
+ PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false),
+ TwoFactorEnabled = table.Column(type: "boolean", nullable: false),
+ LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true),
+ LockoutEnabled = table.Column(type: "boolean", nullable: false),
+ AccessFailedCount = table.Column(type: "integer", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUsers", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ RoleId = table.Column(type: "text", nullable: false),
+ ClaimType = table.Column(type: "text", nullable: true),
+ ClaimValue = table.Column(type: "text", nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ UserId = table.Column(type: "text", nullable: false),
+ ClaimType = table.Column(type: "text", nullable: true),
+ ClaimValue = table.Column(type: "text", nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(type: "text", nullable: false),
+ ProviderKey = table.Column(type: "text", nullable: false),
+ ProviderDisplayName = table.Column(type: "text", nullable: true),
+ UserId = table.Column(type: "text", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(type: "text", nullable: false),
+ RoleId = table.Column(type: "text", nullable: false),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(type: "text", nullable: false),
+ LoginProvider = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "text", nullable: false),
+ Value = table.Column(type: "text", nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "AspNetRoles",
+ column: "NormalizedName",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "AspNetUsers",
+ column: "NormalizedEmail");
+
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "AspNetUsers",
+ column: "NormalizedUserName",
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs b/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs
new file mode 100644
index 0000000..c713313
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250418071219_AddNames.Designer.cs
@@ -0,0 +1,290 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ [DbContext(typeof(AuthDbContext))]
+ [Migration("20250418071219_AddNames")]
+ partial class AddNames
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MiddleName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs b/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs
new file mode 100644
index 0000000..8d6abf4
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250418071219_AddNames.cs
@@ -0,0 +1,57 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ using Microsoft.EntityFrameworkCore.Migrations;
+
+ ///
+ public partial class AddNames : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "FirstName",
+ table: "AspNetUsers",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: false,
+ defaultValue: string.Empty);
+
+ migrationBuilder.AddColumn(
+ name: "LastName",
+ table: "AspNetUsers",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: false,
+ defaultValue: string.Empty);
+
+ migrationBuilder.AddColumn(
+ name: "MiddleName",
+ table: "AspNetUsers",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "FirstName",
+ table: "AspNetUsers");
+
+ migrationBuilder.DropColumn(
+ name: "LastName",
+ table: "AspNetUsers");
+
+ migrationBuilder.DropColumn(
+ name: "MiddleName",
+ table: "AspNetUsers");
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs
new file mode 100644
index 0000000..9840c54
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.Designer.cs
@@ -0,0 +1,330 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ [DbContext(typeof(AuthDbContext))]
+ [Migration("20250429162411_AddedRefreshToken")]
+ partial class AddedRefreshToken
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MiddleName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Expires")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Revoked")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b =>
+ {
+ b.HasOne("ApplicationUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.HasOne("ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs
new file mode 100644
index 0000000..24db2f3
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/20250429162411_AddedRefreshToken.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) Gleb Kargin. All rights reserved.
+//
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ using System;
+ using Microsoft.EntityFrameworkCore.Migrations;
+ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+ ///
+ public partial class AddedRefreshToken : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "RefreshTokens",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ UserId = table.Column(type: "text", nullable: false),
+ Token = table.Column(type: "text", nullable: false),
+ Expires = table.Column(type: "timestamp with time zone", nullable: false),
+ Revoked = table.Column(type: "timestamp with time zone", nullable: true),
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_RefreshTokens", x => x.Id);
+ table.ForeignKey(
+ name: "FK_RefreshTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RefreshTokens_UserId",
+ table: "RefreshTokens",
+ column: "UserId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "RefreshTokens");
+ }
+ }
+}
diff --git a/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs b/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs
new file mode 100644
index 0000000..46322ef
--- /dev/null
+++ b/Services/AuthService.Api/Migrations/AuthDbContextModelSnapshot.cs
@@ -0,0 +1,327 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AuthService.Api.Migrations
+{
+ [DbContext(typeof(AuthDbContext))]
+ partial class AuthDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ApplicationUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MiddleName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("AuthService.Api.Models.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Expires")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Revoked")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property