using foodmarket.Api.Seed;
using foodmarket.Domain.Organizations;
using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers;
/// Самообслуживание: регистрация новой организации с публичного
/// маркетингового сайта. Создаёт Organization + bootstrap (Stores, Roles,
/// Units, PriceTypes, Cassa) + первого Owner-Employee-AppUser-Admin.
///
/// Токены НЕ выпускаются здесь — фронт получает их обычным запросом
/// /connect/token (password grant) сразу после успешного signup. Это
/// убирает дублирование с OpenIddict и упрощает контракт. Phase 6: без
/// email-верификации (переедет в Phase 7).
[ApiController]
[Route("api/auth")]
public class AuthSignupController : ControllerBase
{
private readonly AppDbContext _db;
private readonly UserManager _userMgr;
public AuthSignupController(AppDbContext db, UserManager userMgr)
{
_db = db; _userMgr = userMgr;
}
public record SignupInput(string Email, string Password, string OrganizationName, string? Phone, string? Plan);
public record SignupResult(Guid OrganizationId, string Email);
[HttpPost("signup")]
public async Task> Signup([FromBody] SignupInput input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input.Email) || string.IsNullOrWhiteSpace(input.Password)
|| string.IsNullOrWhiteSpace(input.OrganizationName))
return BadRequest(new { error = "Email, пароль и название обязательны." });
if (input.Password.Length < 8)
return BadRequest(new { error = "Пароль минимум 8 символов." });
var existing = await _userMgr.FindByEmailAsync(input.Email);
if (existing is not null)
return BadRequest(new { error = "Пользователь с таким email уже зарегистрирован." });
// 1. Organization + полный bootstrap tenant-сущностей.
var kzt = await _db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct);
var org = new Organization
{
Name = input.OrganizationName.Trim(),
CountryCode = "KZ",
DefaultCurrencyId = kzt?.Id,
Phone = string.IsNullOrWhiteSpace(input.Phone) ? null : input.Phone.Trim(),
Email = input.Email.Trim(),
};
_db.Organizations.Add(org);
await _db.SaveChangesAsync(ct);
await DevDataSeeder.SeedTenantReferencesAsync(_db, org.Id, ct);
// 2. AppUser в роли Identity Admin, привязан к этой организации.
var user = new User
{
UserName = input.Email.Trim(),
Email = input.Email.Trim(),
EmailConfirmed = true,
FullName = input.OrganizationName.Trim(),
OrganizationId = org.Id,
IsActive = true,
};
var ur = await _userMgr.CreateAsync(user, input.Password);
if (!ur.Succeeded)
{
// Откат: убираем органзацию чтобы не оставить orphan.
_db.Organizations.Remove(org);
await _db.SaveChangesAsync(ct);
return BadRequest(new { error = string.Join("; ", ur.Errors.Select(e => e.Description)) });
}
await _userMgr.AddToRoleAsync(user, "Admin");
// 3. Owner Employee с системной ролью «Администратор».
var adminRole = await _db.EmployeeRoles.IgnoreQueryFilters()
.FirstAsync(r => r.OrganizationId == org.Id && r.IsSystem && r.Name == "Администратор", ct);
_db.Employees.Add(new Employee
{
OrganizationId = org.Id, UserId = user.Id,
LastName = input.OrganizationName.Trim(), FirstName = "Owner",
Position = "Владелец", Email = input.Email.Trim(),
RoleId = adminRole.Id, IsActive = true,
});
org.AccountOwnerUserId = user.Id;
await _db.SaveChangesAsync(ct);
return new SignupResult(org.Id, user.Email);
}
}