Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Таблица токенов устройств (device_tokens, Phase40) + POST/DELETE /api/me/device-tokens для регистрации iOS/Android/Web push-токенов. Контракт IPushSender.SendAsync; в DI включён StubPushSender (пишет в лог) — реальный APNs/FCM/VAPID подменит заглушку, когда появятся ключи, без правок вызывающего кода. Описано в docs/mobile-api.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.9 KiB
C#
70 lines
2.9 KiB
C#
using foodmarket.Application.Common.Tenancy;
|
|
using foodmarket.Domain.Organizations;
|
|
using foodmarket.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace foodmarket.Api.Controllers;
|
|
|
|
/// <summary>Регистрация токенов устройств для push (iOS/Android/Web). Приложение/PWA
|
|
/// вызывает после получения разрешения. Реальная отправка пока заглушка — см.
|
|
/// IPushSender/StubPushSender; таблица и регистрация заложены заранее.</summary>
|
|
[ApiController]
|
|
[Authorize]
|
|
[Route("api/me/device-tokens")]
|
|
public class DeviceTokensController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ITenantContext _tenant;
|
|
|
|
public DeviceTokensController(AppDbContext db, ITenantContext tenant)
|
|
{
|
|
_db = db;
|
|
_tenant = tenant;
|
|
}
|
|
|
|
public record RegisterInput(string Platform, string Token);
|
|
|
|
/// <summary>Зарегистрировать/обновить токен устройства текущего пользователя.
|
|
/// Идемпотентно по токену (повторный вызов обновляет LastSeenAt/владельца).</summary>
|
|
[HttpPost]
|
|
public async Task<IActionResult> Register([FromBody] RegisterInput input, CancellationToken ct)
|
|
{
|
|
var orgId = _tenant.OrganizationId;
|
|
var userId = _tenant.UserId;
|
|
if (orgId is null || userId is null) return Unauthorized();
|
|
if (string.IsNullOrWhiteSpace(input.Token)) return BadRequest(new { error = "Пустой токен." });
|
|
|
|
var platform = string.IsNullOrWhiteSpace(input.Platform) ? "ios" : input.Platform.Trim().ToLowerInvariant();
|
|
var now = DateTime.UtcNow;
|
|
var existing = await _db.DeviceTokens.FirstOrDefaultAsync(t => t.Token == input.Token, ct);
|
|
if (existing is null)
|
|
{
|
|
_db.DeviceTokens.Add(new DeviceToken
|
|
{
|
|
OrganizationId = orgId.Value, UserId = userId.Value,
|
|
Platform = platform, Token = input.Token.Trim(), LastSeenAt = now,
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existing.OrganizationId = orgId.Value;
|
|
existing.UserId = userId.Value;
|
|
existing.Platform = platform;
|
|
existing.LastSeenAt = now;
|
|
}
|
|
await _db.SaveChangesAsync(ct);
|
|
return NoContent();
|
|
}
|
|
|
|
/// <summary>Удалить токен (при выходе/отзыве разрешения).</summary>
|
|
[HttpDelete]
|
|
public async Task<IActionResult> Unregister([FromQuery] string token, CancellationToken ct)
|
|
{
|
|
var existing = await _db.DeviceTokens.FirstOrDefaultAsync(t => t.Token == token, ct);
|
|
if (existing is not null) { _db.DeviceTokens.Remove(existing); await _db.SaveChangesAsync(ct); }
|
|
return NoContent();
|
|
}
|
|
}
|