Skip to content

Commit 55f80e6

Browse files
Merge pull request #65 from nullinside-development-group/feature/desktop
Feature/desktop
2 parents 3747a59 + e6de83c commit 55f80e6

File tree

5 files changed

+139
-34
lines changed

5 files changed

+139
-34
lines changed

src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public interface ITwitchApiProxy {
1414
/// The Twitch access token. These are the credentials used for all requests.
1515
/// </summary>
1616
TwitchAccessToken? OAuth { get; set; }
17+
18+
/// <summary>
19+
/// The Twitch app configuration. These are used for all requests.
20+
/// </summary>
21+
TwitchAppConfig? TwitchAppConfig { get; set; }
1722

1823
/// <summary>
1924
/// Creates a new access token from a code using Twitch's OAuth workflow.

src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,15 @@ public class TwitchApiProxy : ITwitchApiProxy {
2929
/// </summary>
3030
private static readonly ILog Log = LogManager.GetLogger(typeof(TwitchApiProxy));
3131

32-
/// <summary>
33-
/// The, public, twitch client id.
34-
/// </summary>
35-
private static readonly string ClientId = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID")!;
36-
37-
/// <summary>
38-
/// The, private, twitch client secret.
39-
/// </summary>
40-
private static readonly string ClientSecret = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET")!;
41-
42-
/// <summary>
43-
/// The redirect url.
44-
/// </summary>
45-
private static readonly string ClientRedirect = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")!;
46-
4732
/// <summary>
4833
/// Initializes a new instance of the <see cref="TwitchApiProxy" /> class.
4934
/// </summary>
5035
public TwitchApiProxy() {
36+
TwitchAppConfig = new() {
37+
ClientId = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID"),
38+
ClientSecret = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET"),
39+
ClientRedirect = Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")
40+
};
5141
}
5242

5343
/// <summary>
@@ -56,12 +46,25 @@ public TwitchApiProxy() {
5646
/// <param name="token">The access token.</param>
5747
/// <param name="refreshToken">The refresh token.</param>
5848
/// <param name="tokenExpires">When the token expires (utc).</param>
59-
public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires) {
49+
/// <param name="clientId">The client id of the registered twitch app, uses environment variable
50+
/// "TWITCH_BOT_CLIENT_ID" when null.</param>
51+
/// <param name="clientSecret">The client secret of the registered twitch app, uses environment variable
52+
/// "TWITCH_BOT_CLIENT_SECRET" when null.</param>
53+
/// <param name="clientRedirect">The url to redirect to from the registered twitch app, uses environment variable
54+
/// "TWITCH_BOT_CLIENT_REDIRECT" when null.</param>
55+
public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires, string? clientId = null,
56+
string? clientSecret = null, string? clientRedirect = null) {
6057
OAuth = new TwitchAccessToken {
6158
AccessToken = token,
6259
RefreshToken = refreshToken,
6360
ExpiresUtc = tokenExpires
6461
};
62+
63+
TwitchAppConfig = new() {
64+
ClientId = clientId ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_ID"),
65+
ClientSecret = clientSecret ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_SECRET"),
66+
ClientRedirect = clientRedirect ?? Environment.GetEnvironmentVariable("TWITCH_BOT_CLIENT_REDIRECT")
67+
};
6568
}
6669

6770
/// <summary>
@@ -70,12 +73,16 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
7073
public int Retries { get; set; } = 3;
7174

7275
/// <inheritdoc />
73-
public TwitchAccessToken? OAuth { get; set; }
76+
public virtual TwitchAccessToken? OAuth { get; set; }
77+
78+
/// <inheritdoc />
79+
public virtual TwitchAppConfig? TwitchAppConfig { get; set; }
7480

7581
/// <inheritdoc />
76-
public async Task<TwitchAccessToken?> CreateAccessToken(string code, CancellationToken token = new()) {
82+
public virtual async Task<TwitchAccessToken?> CreateAccessToken(string code, CancellationToken token = new()) {
7783
ITwitchAPI api = GetApi();
78-
AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, ClientSecret, ClientRedirect);
84+
AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, TwitchAppConfig?.ClientSecret,
85+
TwitchAppConfig?.ClientRedirect);
7986
if (null == response) {
8087
return null;
8188
}
@@ -89,10 +96,14 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
8996
}
9097

9198
/// <inheritdoc />
92-
public async Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new()) {
99+
public virtual async Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new()) {
93100
try {
101+
if (string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientSecret) || string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientId)) {
102+
return null;
103+
}
104+
94105
ITwitchAPI api = GetApi();
95-
RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(OAuth?.RefreshToken, ClientSecret, ClientId);
106+
RefreshResponse? response = await api.Auth.RefreshAuthTokenAsync(OAuth?.RefreshToken, TwitchAppConfig?.ClientSecret, TwitchAppConfig?.ClientId);
96107
if (null == response) {
97108
return null;
98109
}
@@ -111,11 +122,16 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
111122

112123
/// <inheritdoc />
113124
public async Task<bool> GetAccessTokenIsValid(CancellationToken token = new()) {
114-
return !string.IsNullOrWhiteSpace((await GetUser(token)).id);
125+
try {
126+
return !string.IsNullOrWhiteSpace((await GetUser(token)).id);
127+
}
128+
catch {
129+
return false;
130+
}
115131
}
116132

117133
/// <inheritdoc />
118-
public async Task<(string? id, string? username)> GetUser(CancellationToken token = new()) {
134+
public virtual async Task<(string? id, string? username)> GetUser(CancellationToken token = new()) {
119135
return await Retry.Execute(async () => {
120136
ITwitchAPI api = GetApi();
121137
GetUsersResponse? response = await api.Helix.Users.GetUsersAsync();
@@ -129,7 +145,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
129145
}
130146

131147
/// <inheritdoc />
132-
public async Task<string?> GetUserEmail(CancellationToken token = new()) {
148+
public virtual async Task<string?> GetUserEmail(CancellationToken token = new()) {
133149
return await Retry.Execute(async () => {
134150
ITwitchAPI api = GetApi();
135151
GetUsersResponse? response = await api.Helix.Users.GetUsersAsync();
@@ -142,7 +158,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires)
142158
}
143159

144160
/// <inheritdoc />
145-
public async Task<IEnumerable<TwitchModeratedChannel>> GetUserModChannels(string userId) {
161+
public virtual async Task<IEnumerable<TwitchModeratedChannel>> GetUserModChannels(string userId) {
146162
using var client = new HttpClient();
147163

148164
var ret = new List<TwitchModeratedChannel>();
@@ -155,7 +171,7 @@ public async Task<IEnumerable<TwitchModeratedChannel>> GetUserModChannels(string
155171

156172
var request = new HttpRequestMessage(HttpMethod.Get, url);
157173
request.Headers.Add("Authorization", $"Bearer {OAuth?.AccessToken}");
158-
request.Headers.Add("Client-Id", ClientId);
174+
request.Headers.Add("Client-Id", TwitchAppConfig?.ClientId);
159175

160176
using HttpResponseMessage response = await client.SendAsync(request);
161177
response.EnsureSuccessStatusCode();
@@ -173,7 +189,7 @@ public async Task<IEnumerable<TwitchModeratedChannel>> GetUserModChannels(string
173189
}
174190

175191
/// <inheritdoc />
176-
public async Task<IEnumerable<BannedUser>> BanChannelUsers(string channelId, string botId,
192+
public virtual async Task<IEnumerable<BannedUser>> BanChannelUsers(string channelId, string botId,
177193
IEnumerable<(string Id, string Username)> users, string reason, CancellationToken token = new()) {
178194
return await Retry.Execute(async () => {
179195
ITwitchAPI api = GetApi();
@@ -207,7 +223,7 @@ public async Task<IEnumerable<BannedUser>> BanChannelUsers(string channelId, str
207223
}
208224

209225
/// <inheritdoc />
210-
public async Task<IEnumerable<Chatter>> GetChannelUsers(string channelId, string botId,
226+
public virtual async Task<IEnumerable<Chatter>> GetChannelUsers(string channelId, string botId,
211227
CancellationToken token = new()) {
212228
return await Retry.Execute(async () => {
213229
ITwitchAPI api = GetApi();
@@ -231,7 +247,7 @@ public async Task<IEnumerable<Chatter>> GetChannelUsers(string channelId, string
231247
}
232248

233249
/// <inheritdoc />
234-
public async Task<IEnumerable<string>> GetChannelsLive(IEnumerable<string> userIds) {
250+
public virtual async Task<IEnumerable<string>> GetChannelsLive(IEnumerable<string> userIds) {
235251
ITwitchAPI api = GetApi();
236252

237253
// We can only query 100 at a time, so throttle the search.
@@ -255,7 +271,7 @@ public async Task<IEnumerable<string>> GetChannelsLive(IEnumerable<string> userI
255271
}
256272

257273
/// <inheritdoc />
258-
public async Task<IEnumerable<Moderator>> GetChannelMods(string channelId, CancellationToken token = new()) {
274+
public virtual async Task<IEnumerable<Moderator>> GetChannelMods(string channelId, CancellationToken token = new()) {
259275
return await Retry.Execute(async () => {
260276
ITwitchAPI api = GetApi();
261277

@@ -282,7 +298,7 @@ public async Task<IEnumerable<string>> GetChannelsLive(IEnumerable<string> userI
282298
}
283299

284300
/// <inheritdoc />
285-
public async Task<bool> AddChannelMod(string channelId, string userId, CancellationToken token = new()) {
301+
public virtual async Task<bool> AddChannelMod(string channelId, string userId, CancellationToken token = new()) {
286302
return await Retry.Execute(async () => {
287303
ITwitchAPI api = GetApi();
288304
await api.Helix.Moderation.AddChannelModeratorAsync(channelId, userId);
@@ -294,10 +310,10 @@ public async Task<IEnumerable<string>> GetChannelsLive(IEnumerable<string> userI
294310
/// Gets a new instance of the <see cref="TwitchAPI" />.
295311
/// </summary>
296312
/// <returns>A new instance of the <see cref="TwitchAPI" />.</returns>
297-
protected ITwitchAPI GetApi() {
313+
protected virtual ITwitchAPI GetApi() {
298314
var api = new TwitchAPI {
299315
Settings = {
300-
ClientId = ClientId,
316+
ClientId = TwitchAppConfig?.ClientId,
301317
AccessToken = OAuth?.AccessToken
302318
}
303319
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Nullinside.Api.Common.Twitch;
2+
3+
/// <summary>
4+
/// The configuration for a twitch app that provides OAuth tokens.
5+
/// </summary>
6+
public class TwitchAppConfig {
7+
/// <summary>
8+
/// The client id.
9+
/// </summary>
10+
public string? ClientId { get; set; }
11+
12+
/// <summary>
13+
/// The client secret.
14+
/// </summary>
15+
public string? ClientSecret { get; set; }
16+
17+
/// <summary>
18+
/// A registered URL that the Twitch API is allowed to redirect to on our website.
19+
/// </summary>
20+
public string? ClientRedirect { get; set; }
21+
}

src/Nullinside.Api/Controllers/UserController.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,69 @@ public async Task<RedirectResult> TwitchLogin([FromQuery] string code, [FromServ
122122

123123
return Redirect($"{siteUrl}/user/login?token={bearerToken}");
124124
}
125+
126+
/// <summary>
127+
/// **NOT CALLED BY SITE OR USERS** This endpoint is called by twitch as part of their oauth workflow. It
128+
/// redirects users back to the nullinside website.
129+
/// </summary>
130+
/// <param name="code">The credentials provided by twitch.</param>
131+
/// <param name="api">The twitch api.</param>
132+
/// <param name="token">The cancellation token.</param>
133+
/// <returns>
134+
/// A redirect to the nullinside website.
135+
/// Errors:
136+
/// 2 = Internal error generating token.
137+
/// 3 = Code was invalid
138+
/// 4 = Twitch account has no email
139+
/// </returns>
140+
[AllowAnonymous]
141+
[HttpGet]
142+
[Route("twitch-login/twitch-streaming-tools")]
143+
public async Task<RedirectResult> TwitchStreamingToolsLogin([FromQuery] string code, [FromServices] ITwitchApiProxy api,
144+
CancellationToken token = new()) {
145+
string? siteUrl = _configuration.GetValue<string>("Api:SiteUrl");
146+
if (null == await api.CreateAccessToken(code, token)) {
147+
return Redirect($"{siteUrl}/user/login/desktop?error=3");
148+
}
149+
150+
return Redirect($"{siteUrl}/user/login/desktop?bearer={api.OAuth?.AccessToken}&refresh={api.OAuth?.RefreshToken}&expiresUtc={api.OAuth?.ExpiresUtc?.ToString()}");
151+
}
152+
153+
/// <summary>
154+
/// Used to refresh OAuth tokens from the desktop application.
155+
/// </summary>
156+
/// <param name="refreshToken">The oauth refresh token provided by twitch.</param>
157+
/// <param name="api">The twitch api.</param>
158+
/// <param name="token">The cancellation token.</param>
159+
/// <returns>
160+
/// A redirect to the nullinside website.
161+
/// Errors:
162+
/// 2 = Internal error generating token.
163+
/// 3 = Code was invalid
164+
/// 4 = Twitch account has no email
165+
/// </returns>
166+
[AllowAnonymous]
167+
[HttpPost]
168+
[Route("twitch-login/twitch-streaming-tools")]
169+
public async Task<IActionResult> TwitchStreamingToolsRefreshToken(string refreshToken, [FromServices] ITwitchApiProxy api,
170+
CancellationToken token = new()) {
171+
string? siteUrl = _configuration.GetValue<string>("Api:SiteUrl");
172+
api.OAuth = new() {
173+
AccessToken = null,
174+
RefreshToken = refreshToken,
175+
ExpiresUtc = DateTime.MinValue
176+
};
177+
178+
if (null == await api.RefreshAccessToken(token)) {
179+
return this.BadRequest();
180+
}
181+
182+
return Ok(new {
183+
bearer = api.OAuth.AccessToken,
184+
refresh = api.OAuth.RefreshToken,
185+
expiresUtc = api.OAuth.ExpiresUtc
186+
});
187+
}
125188

126189
/// <summary>
127190
/// Gets the roles of the current user.

src/Nullinside.Api/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
builder.EnableRetryOnFailure(3);
3232
}));
3333
builder.Services.AddScoped<IAuthorizationHandler, BasicAuthorizationHandler>();
34-
builder.Services.AddScoped<ITwitchApiProxy, TwitchApiProxy>();
34+
builder.Services.AddTransient<ITwitchApiProxy, TwitchApiProxy>();
3535
builder.Services.AddAuthentication()
3636
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Bearer", _ => { });
3737
builder.Services.AddScoped<IDockerProxy, DockerProxy>();

0 commit comments

Comments
 (0)