// 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);
}