diff --git a/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs b/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs
index f5d651c..bcb3ca9 100644
--- a/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs
+++ b/src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs
@@ -14,6 +14,11 @@ public interface ITwitchApiProxy {
/// The Twitch access token. These are the credentials used for all requests.
///
TwitchAccessToken? OAuth { get; set; }
+
+ ///
+ /// The Twitch app configuration. These are used for all requests.
+ ///
+ TwitchAppConfig? TwitchAppConfig { get; set; }
///
/// Creates a new access token from a code using Twitch's OAuth workflow.
diff --git a/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs b/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs
index 56a3ec4..fc0f708 100644
--- a/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs
+++ b/src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs
@@ -29,25 +29,15 @@ public class TwitchApiProxy : ITwitchApiProxy {
///
private static readonly ILog Log = LogManager.GetLogger(typeof(TwitchApiProxy));
- ///
- /// The, public, twitch client id.
- ///
- private static readonly string ClientId = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID")!;
-
- ///
- /// The, private, twitch client secret.
- ///
- private static readonly string ClientSecret = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET")!;
-
- ///
- /// The redirect url.
- ///
- private static readonly string ClientRedirect = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")!;
-
///
/// Initializes a new instance of the class.
///
public TwitchApiProxy() {
+ TwitchAppConfig = new() {
+ ClientId = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID"),
+ ClientSecret = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET"),
+ ClientRedirect = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")
+ };
}
///
@@ -56,12 +46,25 @@ public TwitchApiProxy() {
/// The access token.
/// The refresh token.
/// When the token expires (utc).
- public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires) {
+ /// The client id of the registered twitch app, uses environment variable
+ /// "TWITCH_BOT_CLIENT_ID" when null.
+ /// The client secret of the registered twitch app, uses environment variable
+ /// "TWITCH_BOT_CLIENT_SECRET" when null.
+ /// The url to redirect to from the registered twitch app, uses environment variable
+ /// "TWITCH_BOT_CLIENT_REDIRECT" when null.
+ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires, string? clientId = null,
+ string? clientSecret = null, string? clientRedirect = null) {
OAuth = new TwitchAccessToken {
AccessToken = token,
RefreshToken = refreshToken,
ExpiresUtc = tokenExpires
};
+
+ TwitchAppConfig = new() {
+ ClientId = clientId ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID"),
+ ClientSecret = clientSecret ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET"),
+ ClientRedirect = clientRedirect ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")
+ };
}
///
@@ -70,12 +73,16 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
public int Retries { get; set; } = 3;
///
- public TwitchAccessToken? OAuth { get; set; }
+ public virtual TwitchAccessToken? OAuth { get; set; }
+
+ ///
+ public virtual TwitchAppConfig? TwitchAppConfig { get; set; }
///
- public async Task CreateAccessToken(string code, CancellationToken token = new()) {
+ public virtual async Task CreateAccessToken(string code, CancellationToken token = new()) {
ITwitchAPI api = GetApi();
- AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, ClientSecret, ClientRedirect);
+ AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, TwitchAppConfig?.ClientSecret,
+ TwitchAppConfig?.ClientRedirect);
if (null == response) {
return null;
}
@@ -89,10 +96,14 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
}
///
- public async Task RefreshAccessToken(CancellationToken token = new()) {
+ public virtual async Task RefreshAccessToken(CancellationToken token = new()) {
try {
+ if (string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientSecret) || string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientId)) {
+ return null;
+ }
+
ITwitchAPI api = GetApi();
- RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(OAuth?.RefreshToken, ClientSecret, ClientId);
+ RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(OAuth?.RefreshToken, TwitchAppConfig?.ClientSecret, TwitchAppConfig?.ClientId);
if (null == response) {
return null;
}
@@ -111,11 +122,16 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
///
public async Task GetAccessTokenIsValid(CancellationToken token = new()) {
- return !string.IsNullOrWhiteSpace((await GetUser(token)).id);
+ try {
+ return !string.IsNullOrWhiteSpace((await GetUser(token)).id);
+ }
+ catch {
+ return false;
+ }
}
///
- public async Task<(string? id, string? username)> GetUser(CancellationToken token = new()) {
+ public virtual async Task<(string? id, string? username)> GetUser(CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
GetUsersResponse? response = await api.Helix.Users.GetUsersAsync();
@@ -129,7 +145,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
}
///
- public async Task GetUserEmail(CancellationToken token = new()) {
+ public virtual async Task GetUserEmail(CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
GetUsersResponse? response = await api.Helix.Users.GetUsersAsync();
@@ -142,7 +158,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
}
///
- public async Task> GetUserModChannels(string userId) {
+ public virtual async Task> GetUserModChannels(string userId) {
using var client = new HttpClient();
var ret = new List();
@@ -155,7 +171,7 @@ public async Task> GetUserModChannels(string
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Authorization", $"Bearer {OAuth?.AccessToken}");
- request.Headers.Add("Client-Id", ClientId);
+ request.Headers.Add("Client-Id", TwitchAppConfig?.ClientId);
using HttpResponseMessage response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
@@ -173,7 +189,7 @@ public async Task> GetUserModChannels(string
}
///
- public async Task> BanChannelUsers(string channelId, string botId,
+ public virtual async Task> BanChannelUsers(string channelId, string botId,
IEnumerable<(string Id, string Username)> users, string reason, CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
@@ -207,7 +223,7 @@ public async Task> BanChannelUsers(string channelId, str
}
///
- public async Task> GetChannelUsers(string channelId, string botId,
+ public virtual async Task> GetChannelUsers(string channelId, string botId,
CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
@@ -231,7 +247,7 @@ public async Task> GetChannelUsers(string channelId, string
}
///
- public async Task> GetChannelsLive(IEnumerable userIds) {
+ public virtual async Task> GetChannelsLive(IEnumerable userIds) {
ITwitchAPI api = GetApi();
// We can only query 100 at a time, so throttle the search.
@@ -255,7 +271,7 @@ public async Task> GetChannelsLive(IEnumerable userI
}
///
- public async Task> GetChannelMods(string channelId, CancellationToken token = new()) {
+ public virtual async Task> GetChannelMods(string channelId, CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
@@ -282,7 +298,7 @@ public async Task> GetChannelsLive(IEnumerable userI
}
///
- public async Task AddChannelMod(string channelId, string userId, CancellationToken token = new()) {
+ public virtual async Task AddChannelMod(string channelId, string userId, CancellationToken token = new()) {
return await Retry.Execute(async () => {
ITwitchAPI api = GetApi();
await api.Helix.Moderation.AddChannelModeratorAsync(channelId, userId);
@@ -294,10 +310,10 @@ public async Task> GetChannelsLive(IEnumerable userI
/// Gets a new instance of the .
///
/// A new instance of the .
- protected ITwitchAPI GetApi() {
+ protected virtual ITwitchAPI GetApi() {
var api = new TwitchAPI {
Settings = {
- ClientId = ClientId,
+ ClientId = TwitchAppConfig?.ClientId,
AccessToken = OAuth?.AccessToken
}
};
diff --git a/src/Nullinside.Api.Common/Twitch/TwitchAppConfig.cs b/src/Nullinside.Api.Common/Twitch/TwitchAppConfig.cs
new file mode 100644
index 0000000..4e5222c
--- /dev/null
+++ b/src/Nullinside.Api.Common/Twitch/TwitchAppConfig.cs
@@ -0,0 +1,21 @@
+namespace Nullinside.Api.Common.Twitch;
+
+///
+/// The configuration for a twitch app that provides OAuth tokens.
+///
+public class TwitchAppConfig {
+ ///
+ /// The client id.
+ ///
+ public string? ClientId { get; set; }
+
+ ///
+ /// The client secret.
+ ///
+ public string? ClientSecret { get; set; }
+
+ ///
+ /// A registered URL that the Twitch API is allowed to redirect to on our website.
+ ///
+ public string? ClientRedirect { get; set; }
+}
\ No newline at end of file
diff --git a/src/Nullinside.Api/Controllers/UserController.cs b/src/Nullinside.Api/Controllers/UserController.cs
index a89a2a2..e112c58 100644
--- a/src/Nullinside.Api/Controllers/UserController.cs
+++ b/src/Nullinside.Api/Controllers/UserController.cs
@@ -122,6 +122,69 @@ public async Task TwitchLogin([FromQuery] string code, [FromServ
return Redirect($"{siteUrl}/user/login?token={bearerToken}");
}
+
+ ///
+ /// **NOT CALLED BY SITE OR USERS** This endpoint is called by twitch as part of their oauth workflow. It
+ /// redirects users back to the nullinside website.
+ ///
+ /// The credentials provided by twitch.
+ /// The twitch api.
+ /// The cancellation token.
+ ///
+ /// A redirect to the nullinside website.
+ /// Errors:
+ /// 2 = Internal error generating token.
+ /// 3 = Code was invalid
+ /// 4 = Twitch account has no email
+ ///
+ [AllowAnonymous]
+ [HttpGet]
+ [Route("twitch-login/twitch-streaming-tools")]
+ public async Task TwitchStreamingToolsLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api,
+ CancellationToken token = new()) {
+ string? siteUrl = _configuration.GetValue("Api:SiteUrl");
+ if (null == await api.CreateAccessToken(code, token)) {
+ return Redirect($"{siteUrl}/user/login/desktop?error=3");
+ }
+
+ return Redirect($"{siteUrl}/user/login/desktop?bearer={api.OAuth?.AccessToken}&refresh={api.OAuth?.RefreshToken}&expiresUtc={api.OAuth?.ExpiresUtc?.ToString()}");
+ }
+
+ ///
+ /// Used to refresh OAuth tokens from the desktop application.
+ ///
+ /// The oauth refresh token provided by twitch.
+ /// The twitch api.
+ /// The cancellation token.
+ ///
+ /// A redirect to the nullinside website.
+ /// Errors:
+ /// 2 = Internal error generating token.
+ /// 3 = Code was invalid
+ /// 4 = Twitch account has no email
+ ///
+ [AllowAnonymous]
+ [HttpPost]
+ [Route("twitch-login/twitch-streaming-tools")]
+ public async Task TwitchStreamingToolsRefreshToken(string refreshToken, [FromServices] ITwitchApiProxy api,
+ CancellationToken token = new()) {
+ string? siteUrl = _configuration.GetValue("Api:SiteUrl");
+ api.OAuth = new() {
+ AccessToken = null,
+ RefreshToken = refreshToken,
+ ExpiresUtc = DateTime.MinValue
+ };
+
+ if (null == await api.RefreshAccessToken(token)) {
+ return this.BadRequest();
+ }
+
+ return Ok(new {
+ bearer = api.OAuth.AccessToken,
+ refresh = api.OAuth.RefreshToken,
+ expiresUtc = api.OAuth.ExpiresUtc
+ });
+ }
///
/// Gets the roles of the current user.
diff --git a/src/Nullinside.Api/Program.cs b/src/Nullinside.Api/Program.cs
index 7ad9380..9d12fcf 100644
--- a/src/Nullinside.Api/Program.cs
+++ b/src/Nullinside.Api/Program.cs
@@ -31,7 +31,7 @@
builder.EnableRetryOnFailure(3);
}));
builder.Services.AddScoped();
-builder.Services.AddScoped();
+builder.Services.AddTransient();
builder.Services.AddAuthentication()
.AddScheme("Bearer", _ => { });
builder.Services.AddScoped();