feat(api): .NET 10 multi-tenant REST API
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Meezi.API.Models.Auth;
|
||||
using Meezi.API.Services;
|
||||
using Meezi.Core.Constants;
|
||||
using Meezi.Shared;
|
||||
|
||||
namespace Meezi.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IValidator<SendOtpRequest> _sendOtpValidator;
|
||||
private readonly IValidator<VerifyOtpRequest> _verifyOtpValidator;
|
||||
private readonly IValidator<RefreshTokenRequest> _refreshValidator;
|
||||
|
||||
public AuthController(
|
||||
IAuthService authService,
|
||||
IValidator<SendOtpRequest> sendOtpValidator,
|
||||
IValidator<VerifyOtpRequest> verifyOtpValidator,
|
||||
IValidator<RefreshTokenRequest> refreshValidator)
|
||||
{
|
||||
_authService = authService;
|
||||
_sendOtpValidator = sendOtpValidator;
|
||||
_verifyOtpValidator = verifyOtpValidator;
|
||||
_refreshValidator = refreshValidator;
|
||||
}
|
||||
|
||||
[HttpPost("send-otp")]
|
||||
[EnableRateLimiting("auth-otp")]
|
||||
[ProducesResponseType(typeof(ApiResponse<SendOtpResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SendOtp([FromBody] SendOtpRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await _sendOtpValidator.ValidateAsync(request, cancellationToken);
|
||||
if (!validation.IsValid)
|
||||
return BadRequest(ValidationError(validation));
|
||||
|
||||
var (success, data, code, message) = await _authService.SendOtpAsync(request, cancellationToken);
|
||||
if (!success)
|
||||
return ErrorResult(code!, message!);
|
||||
|
||||
return Ok(new ApiResponse<SendOtpResponse>(true, data));
|
||||
}
|
||||
|
||||
[HttpPost("verify-otp")]
|
||||
[ProducesResponseType(typeof(ApiResponse<AuthTokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> VerifyOtp([FromBody] VerifyOtpRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await _verifyOtpValidator.ValidateAsync(request, cancellationToken);
|
||||
if (!validation.IsValid)
|
||||
return BadRequest(ValidationError(validation));
|
||||
|
||||
var (success, data, code, message) = await _authService.VerifyOtpAsync(request, cancellationToken);
|
||||
if (!success)
|
||||
return ErrorResult(code!, message!);
|
||||
|
||||
return Ok(new ApiResponse<AuthTokenResponse>(true, data));
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
[ProducesResponseType(typeof(ApiResponse<AuthTokenResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Refresh([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var validation = await _refreshValidator.ValidateAsync(request, cancellationToken);
|
||||
if (!validation.IsValid)
|
||||
return BadRequest(ValidationError(validation));
|
||||
|
||||
var (success, data, code, message) = await _authService.RefreshAsync(request, cancellationToken);
|
||||
if (!success)
|
||||
return ErrorResult(code!, message!);
|
||||
|
||||
return Ok(new ApiResponse<AuthTokenResponse>(true, data));
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(typeof(ApiResponse<AuthTokenResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public IActionResult GetMe()
|
||||
{
|
||||
var userId = User.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||
?? User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? string.Empty;
|
||||
|
||||
var expClaim = User.FindFirstValue(JwtRegisteredClaimNames.Exp);
|
||||
var expiresAt = expClaim != null && long.TryParse(expClaim, out var exp)
|
||||
? DateTimeOffset.FromUnixTimeSeconds(exp).UtcDateTime
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var data = new AuthTokenResponse(
|
||||
AccessToken: string.Empty,
|
||||
RefreshToken: string.Empty,
|
||||
ExpiresAt: expiresAt,
|
||||
UserId: userId,
|
||||
CafeId: User.FindFirstValue(MeeziClaimTypes.CafeId) ?? string.Empty,
|
||||
Role: User.FindFirstValue(MeeziClaimTypes.Role) ?? string.Empty,
|
||||
PlanTier: User.FindFirstValue(MeeziClaimTypes.PlanTier) ?? string.Empty,
|
||||
Language: User.FindFirstValue(MeeziClaimTypes.Language) ?? string.Empty,
|
||||
Actor: User.FindFirstValue(MeeziClaimTypes.Actor) ?? MeeziActorKinds.Merchant,
|
||||
BranchId: User.FindFirstValue(MeeziClaimTypes.BranchId));
|
||||
|
||||
return Ok(new ApiResponse<AuthTokenResponse>(true, data));
|
||||
}
|
||||
|
||||
private static ApiResponse<object> ValidationError(FluentValidation.Results.ValidationResult validation)
|
||||
{
|
||||
var first = validation.Errors.First();
|
||||
return new ApiResponse<object>(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName));
|
||||
}
|
||||
|
||||
private IActionResult ErrorResult(string code, string message) => code switch
|
||||
{
|
||||
"RATE_LIMITED" => StatusCode(StatusCodes.Status429TooManyRequests,
|
||||
new ApiResponse<object>(false, null, new ApiError(code, message))),
|
||||
"NOT_FOUND" => NotFound(new ApiResponse<object>(false, null, new ApiError(code, message))),
|
||||
"INVALID_OTP" or "INVALID_TOKEN" => Unauthorized(new ApiResponse<object>(false, null, new ApiError(code, message))),
|
||||
_ => BadRequest(new ApiResponse<object>(false, null, new ApiError(code, message)))
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user