|
| 1 | +using Microsoft.AspNetCore.Mvc; |
| 2 | +using TelegramDownloader.Data; |
| 3 | +using TelegramDownloader.Models.Api; |
| 4 | +using TelegramDownloader.Services; |
| 5 | +using TelegramDownloader.Services.Api; |
| 6 | + |
| 7 | +namespace TelegramDownloader.Controllers.Api.V1 |
| 8 | +{ |
| 9 | + /// <summary> |
| 10 | + /// Telegram session lifecycle: sign in with a phone number or a QR code, |
| 11 | + /// inspect the current session and sign out. |
| 12 | + /// |
| 13 | + /// The Telegram session lives on the server and is shared by the web UI and |
| 14 | + /// every API client: signing in here also signs in the web UI, and signing |
| 15 | + /// out terminates both. |
| 16 | + /// </summary> |
| 17 | + [Route("api/v1/auth")] |
| 18 | + [Tags("Auth")] |
| 19 | + public class AuthController : ApiV1ControllerBase |
| 20 | + { |
| 21 | + private readonly ITelegramService _telegram; |
| 22 | + private readonly ISetupService _setup; |
| 23 | + private readonly QrLoginSessionManager _qr; |
| 24 | + private readonly ILogger<AuthController> _logger; |
| 25 | + |
| 26 | + public AuthController( |
| 27 | + ITelegramService telegram, |
| 28 | + ISetupService setup, |
| 29 | + QrLoginSessionManager qr, |
| 30 | + ILogger<AuthController> logger) |
| 31 | + { |
| 32 | + _telegram = telegram; |
| 33 | + _setup = setup; |
| 34 | + _qr = qr; |
| 35 | + _logger = logger; |
| 36 | + } |
| 37 | + |
| 38 | + /// <summary>Current authentication state.</summary> |
| 39 | + /// <remarks> |
| 40 | + /// Call this first. <c>Step</c> tells you what the server expects next: |
| 41 | + /// <c>phone</c>, <c>vc</c> (verification code), <c>pass</c> (2FA |
| 42 | + /// password), <c>ok</c> (already signed in) or <c>setup_required</c> |
| 43 | + /// when the application has not been configured yet. |
| 44 | + /// </remarks> |
| 45 | + [HttpGet("status")] |
| 46 | + [ProducesResponseType(typeof(ApiResult<AuthStatusDto>), StatusCodes.Status200OK)] |
| 47 | + public async Task<IActionResult> Status() |
| 48 | + { |
| 49 | + try |
| 50 | + { |
| 51 | + var dto = new AuthStatusDto { IsConfigured = _telegram.IsConfigured }; |
| 52 | + |
| 53 | + if (!_telegram.IsConfigured) |
| 54 | + { |
| 55 | + try |
| 56 | + { |
| 57 | + _telegram.InitializeClient(); |
| 58 | + dto.IsConfigured = _telegram.IsConfigured; |
| 59 | + } |
| 60 | + catch (Exception ex) |
| 61 | + { |
| 62 | + _logger.LogDebug(ex, "Telegram client could not be initialized"); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + if (!dto.IsConfigured) |
| 67 | + { |
| 68 | + dto.Step = AuthStep.SetupRequired; |
| 69 | + return OkResult(dto); |
| 70 | + } |
| 71 | + |
| 72 | + dto.Step = await _telegram.checkAuth(null) ?? AuthStep.Phone; |
| 73 | + dto.IsAuthenticated = dto.Step == AuthStep.Authenticated; |
| 74 | + |
| 75 | + if (dto.IsAuthenticated) |
| 76 | + dto.User = await BuildUserAsync(); |
| 77 | + |
| 78 | + return OkResult(dto); |
| 79 | + } |
| 80 | + catch (Exception ex) |
| 81 | + { |
| 82 | + _logger.LogError(ex, "Error reading auth status"); |
| 83 | + return ErrorResult("Could not read the authentication status", ex); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /// <summary>Signed-in Telegram user.</summary> |
| 88 | + [HttpGet("me")] |
| 89 | + [ProducesResponseType(typeof(ApiResult<TelegramUserDto>), StatusCodes.Status200OK)] |
| 90 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status401Unauthorized)] |
| 91 | + public async Task<IActionResult> Me() |
| 92 | + { |
| 93 | + if (!_telegram.IsConfigured || !_telegram.checkUserLogin()) |
| 94 | + return StatusCode(StatusCodes.Status401Unauthorized, |
| 95 | + ApiResult.Fail(ApiErrorCodes.NotLoggedIn, "No Telegram session is active")); |
| 96 | + |
| 97 | + var user = await BuildUserAsync(); |
| 98 | + if (user == null) |
| 99 | + return NotFoundResult("The Telegram user could not be resolved"); |
| 100 | + |
| 101 | + return OkResult(user); |
| 102 | + } |
| 103 | + |
| 104 | + /// <summary>Advances the phone login flow one step.</summary> |
| 105 | + /// <remarks> |
| 106 | + /// Post the phone number with <c>isPhone: true</c> to start. The response |
| 107 | + /// tells you the next step; post the verification code (and then, when |
| 108 | + /// required, the two-factor password) with <c>isPhone: false</c>. |
| 109 | + /// |
| 110 | + /// Sample sequence: |
| 111 | + /// <code> |
| 112 | + /// POST /api/v1/auth/login { "value": "+34600000000", "isPhone": true } -> step "vc" |
| 113 | + /// POST /api/v1/auth/login { "value": "12345" } -> step "pass" or "ok" |
| 114 | + /// POST /api/v1/auth/login { "value": "my-2fa-password" } -> step "ok" |
| 115 | + /// </code> |
| 116 | + /// </remarks> |
| 117 | + [HttpPost("login")] |
| 118 | + [ProducesResponseType(typeof(ApiResult<AuthStatusDto>), StatusCodes.Status200OK)] |
| 119 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status400BadRequest)] |
| 120 | + public async Task<IActionResult> Login([FromBody] LoginStepRequest request) |
| 121 | + { |
| 122 | + if (request == null || string.IsNullOrWhiteSpace(request.Value)) |
| 123 | + return BadRequestResult("A value is required for the current login step"); |
| 124 | + |
| 125 | + try |
| 126 | + { |
| 127 | + if (!_telegram.IsConfigured) |
| 128 | + _telegram.InitializeClient(); |
| 129 | + |
| 130 | + var step = await _telegram.checkAuth(request.Value.Trim(), request.IsPhone) ?? AuthStep.Phone; |
| 131 | + |
| 132 | + var dto = new AuthStatusDto |
| 133 | + { |
| 134 | + Step = step, |
| 135 | + IsConfigured = _telegram.IsConfigured, |
| 136 | + IsAuthenticated = step == AuthStep.Authenticated |
| 137 | + }; |
| 138 | + if (dto.IsAuthenticated) |
| 139 | + dto.User = await BuildUserAsync(); |
| 140 | + |
| 141 | + return OkResult(dto); |
| 142 | + } |
| 143 | + catch (Exception ex) |
| 144 | + { |
| 145 | + _logger.LogWarning(ex, "Login step failed"); |
| 146 | + return BadRequestResult("The login step was rejected by Telegram", ApiErrorCodes.InvalidRequest, ex.Message); |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + /// <summary>Starts a QR login session.</summary> |
| 151 | + /// <remarks> |
| 152 | + /// Render <c>qrImageBase64</c> (a PNG) or encode <c>loginUrl</c> yourself, |
| 153 | + /// then poll <c>GET /api/v1/auth/qr/{sessionId}</c>. Telegram rotates the |
| 154 | + /// token every ~30 seconds, so keep repainting the QR from the polled |
| 155 | + /// value. When the status turns <c>password_required</c>, post the 2FA |
| 156 | + /// password to <c>/api/v1/auth/qr/{sessionId}/password</c>. |
| 157 | + /// </remarks> |
| 158 | + /// <param name="logoutFirst">Terminate any existing session before starting.</param> |
| 159 | + [HttpPost("qr")] |
| 160 | + [ProducesResponseType(typeof(ApiResult<QrLoginDto>), StatusCodes.Status200OK)] |
| 161 | + public async Task<IActionResult> StartQr([FromQuery] bool logoutFirst = false) |
| 162 | + { |
| 163 | + try |
| 164 | + { |
| 165 | + if (!_telegram.IsConfigured) |
| 166 | + _telegram.InitializeClient(); |
| 167 | + |
| 168 | + var session = await _qr.StartAsync(_telegram, logoutFirst); |
| 169 | + return OkResult(session); |
| 170 | + } |
| 171 | + catch (Exception ex) |
| 172 | + { |
| 173 | + _logger.LogError(ex, "Could not start a QR login session"); |
| 174 | + return ErrorResult("Could not start a QR login session", ex); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + /// <summary>Polls the state of a QR login session.</summary> |
| 179 | + [HttpGet("qr/{sessionId}")] |
| 180 | + [ProducesResponseType(typeof(ApiResult<QrLoginDto>), StatusCodes.Status200OK)] |
| 181 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status404NotFound)] |
| 182 | + public IActionResult PollQr(string sessionId) |
| 183 | + { |
| 184 | + var session = _qr.Get(sessionId); |
| 185 | + if (session == null) |
| 186 | + return NotFoundResult("Unknown or expired QR login session"); |
| 187 | + return OkResult(session); |
| 188 | + } |
| 189 | + |
| 190 | + /// <summary>Supplies the two-factor password a QR session is waiting for.</summary> |
| 191 | + [HttpPost("qr/{sessionId}/password")] |
| 192 | + [ProducesResponseType(typeof(ApiResult<QrLoginDto>), StatusCodes.Status200OK)] |
| 193 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status404NotFound)] |
| 194 | + public IActionResult ProvideQrPassword(string sessionId, [FromBody] QrPasswordRequest request) |
| 195 | + { |
| 196 | + if (request == null || string.IsNullOrEmpty(request.Password)) |
| 197 | + return BadRequestResult("A password is required"); |
| 198 | + |
| 199 | + if (!_qr.ProvidePassword(sessionId, _telegram, request.Password)) |
| 200 | + return NotFoundResult("Unknown or expired QR login session"); |
| 201 | + |
| 202 | + return OkResult(_qr.Get(sessionId)!); |
| 203 | + } |
| 204 | + |
| 205 | + /// <summary>Cancels a pending QR login session.</summary> |
| 206 | + [HttpDelete("qr/{sessionId}")] |
| 207 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status200OK)] |
| 208 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status404NotFound)] |
| 209 | + public IActionResult CancelQr(string sessionId) |
| 210 | + { |
| 211 | + if (!_qr.Cancel(sessionId)) |
| 212 | + return NotFoundResult("Unknown or expired QR login session"); |
| 213 | + return OkEmpty("QR login session cancelled"); |
| 214 | + } |
| 215 | + |
| 216 | + /// <summary>Signs out of Telegram.</summary> |
| 217 | + /// <remarks> |
| 218 | + /// This terminates the shared server session: the web UI is signed out |
| 219 | + /// too and every client has to authenticate again. |
| 220 | + /// </remarks> |
| 221 | + [HttpPost("logout")] |
| 222 | + [ProducesResponseType(typeof(ApiResult<object>), StatusCodes.Status200OK)] |
| 223 | + public async Task<IActionResult> Logout() |
| 224 | + { |
| 225 | + try |
| 226 | + { |
| 227 | + await _telegram.logOff(); |
| 228 | + return OkEmpty("Signed out"); |
| 229 | + } |
| 230 | + catch (Exception ex) |
| 231 | + { |
| 232 | + _logger.LogError(ex, "Error signing out"); |
| 233 | + return ErrorResult("Could not sign out", ex); |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + private async Task<TelegramUserDto?> BuildUserAsync() |
| 238 | + { |
| 239 | + try |
| 240 | + { |
| 241 | + var user = await _telegram.GetUser(); |
| 242 | + if (user == null) return null; |
| 243 | + return new TelegramUserDto |
| 244 | + { |
| 245 | + Id = user.id, |
| 246 | + Username = user.username, |
| 247 | + FirstName = user.first_name, |
| 248 | + LastName = user.last_name, |
| 249 | + Phone = user.phone, |
| 250 | + IsPremium = TelegramService.isPremium |
| 251 | + }; |
| 252 | + } |
| 253 | + catch (Exception ex) |
| 254 | + { |
| 255 | + _logger.LogDebug(ex, "Could not resolve the Telegram user"); |
| 256 | + return null; |
| 257 | + } |
| 258 | + } |
| 259 | + } |
| 260 | +} |
0 commit comments