// Copyright (C) 2021-present sam (starshines.gay) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using System.Diagnostics.CodeAnalysis; using System.Net; using System.Web; using Catalogger.Backend.Api.Middleware; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database.Redis; using Microsoft.AspNetCore.Mvc; namespace Catalogger.Backend.Api; [Route("/api")] public class AuthController( Config config, RedisService redisService, GuildCache guildCache, ApiCache apiCache, DiscordRequestService discordRequestService ) : ApiControllerBase { private static string StateKey(string state) => $"state:{state}"; [HttpGet("authorize")] public async Task GenerateAuthUrlAsync() { var state = ApiUtils.RandomToken(); await redisService.SetStringAsync(StateKey(state), state, TimeSpan.FromMinutes(30)); var url = $"https://discord.com/oauth2/authorize?response_type=code" + $"&client_id={config.Discord.ApplicationId}&scope=identify+guilds" + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.Web.BaseUrl}/callback")}"; return Redirect(url); } [HttpGet("add-guild/{id}")] public IActionResult AddGuild(ulong id) { var url = $"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}" + "&permissions=537250993&scope=bot+applications.commands" + $"&guild_id={id}"; return Redirect(url); } [HttpPost("callback")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task CallbackAsync([FromBody] CallbackRequest req) { var redisState = await redisService.GetStringAsync(StateKey(req.State), delete: true); if (redisState != req.State) throw new ApiError( HttpStatusCode.BadRequest, ErrorCode.BadRequest, "Invalid OAuth state" ); var (token, user, guilds) = await discordRequestService.RequestDiscordTokenAsync(req.Code); await apiCache.SetUserAsync(user); await apiCache.SetGuildsAsync(user.Id, guilds); return Ok( new CallbackResponse( new ApiUser(user), guilds .Where(g => g.CanManage) .Select(g => new ApiGuild(g, guildCache.Contains(g.Id))), token.DashboardToken ) ); } public record CallbackRequest(string Code, string State); private record CallbackResponse(ApiUser User, IEnumerable Guilds, string Token); [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] private record DiscordTokenResponse(string AccessToken, string? RefreshToken, int ExpiresIn); }