Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Server.Model.Github;

public sealed class GithubEmail
{
[JsonPropertyName("email")]
public string Email { get; set; } = default!;

[JsonPropertyName("primary")]
public bool Primary { get; set; }

[JsonPropertyName("verified")]
public bool Verified { get; set; }

[JsonPropertyName("visibility")]
public string? Visibility { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ public sealed class GithubUserResponse

[JsonPropertyName("login")]
public string Login { get; set; } = default!;

[JsonPropertyName("email")]
public string? Email { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ public GithubApiService(IServiceProvider serviceProvider)
}
}

public async ValueTask<List<GithubEmail>?> GetEmailsByAccessTokenAsync(string accessToken)
{
using (HttpRequestMessage requestMessage = new(HttpMethod.Get, "https://api.github.com/user/emails"))
{
requestMessage.Headers.Accept.Add(new("application/vnd.github+json"));
requestMessage.Headers.UserAgent.ParseAdd("Snap Hutao Server/1.0");
requestMessage.Headers.Authorization = new("Bearer", accessToken);

using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage))
{
if (!responseMessage.IsSuccessStatusCode)
{
return default;
}

return await responseMessage.Content.ReadFromJsonAsync<List<GithubEmail>>();
}
}
}

public async ValueTask<GithubAccessTokenResponse?> GetAccessTokenByRefreshTokenAsync(string refreshToken)
{
string query = $"client_id={githubOptions.ClientId}&client_secret={githubOptions.ClientSecret}&grant_type=refresh_token&refresh_token={refreshToken}";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

using Snap.Hutao.Server.Core;
using Snap.Hutao.Server.Extension;
using Snap.Hutao.Server.Model.Context;
using Snap.Hutao.Server.Model.Entity.Passport;
using Snap.Hutao.Server.Model.Github;
using Snap.Hutao.Server.Model.Passport;
using Snap.Hutao.Server.Option;
using Snap.Hutao.Server.Service.Authorization;
using Snap.Hutao.Server.Service.Discord;
Expand Down Expand Up @@ -33,7 +35,7 @@ public GithubService(IServiceProvider serviceProvider)

public Task<string> RequestAuthUrlAsync(string state)
{
return Task.FromResult($"https://github.com/login/oauth/authorize?client_id={githubOptions.ClientId}&state={HttpUtility.UrlEncode(state)}");
return Task.FromResult($"https://github.com/login/oauth/authorize?client_id={githubOptions.ClientId}&state={HttpUtility.UrlEncode(state)}&scope=read:user%20user:email");
}

public async Task<bool> RefreshTokenAsync(OAuthBindIdentity identity)
Expand Down Expand Up @@ -65,13 +67,61 @@ public async ValueTask<OAuthResult> HandleOAuthCallbackAsync(string code, OAuthB
return OAuthResult.Fail("获取 GitHub 用户信息失败 | Failed to get GitHub user information");
}

if (string.IsNullOrEmpty(userResponse.Email))
{
List<GithubEmail>? emails = await githubApiService.GetEmailsByAccessTokenAsync(accessTokenResponse.AccessToken).ConfigureAwait(false);
userResponse.Email = emails?.FirstOrDefault(e => e.Primary)?.Email ?? emails?.FirstOrDefault()?.Email;
}

OAuthBindIdentity? identity = await this.appDbContext.OAuthBindIdentities.SingleOrDefaultAsync(b => b.ProviderKind == OAuthProviderKind.Github && b.ProviderId == userResponse.NodeId);
if (state.UserId is -1)
{
// Login mode
// Login or register mode
if (identity is null)
{
return OAuthResult.Fail("当前 GitHub 账号未绑定胡桃通行证 | The current GitHub account is not bound to Snap Hutao Passport");
if (string.IsNullOrEmpty(userResponse.Email))
{
return OAuthResult.Fail("无法获取 GitHub 邮箱 | Failed to get GitHub email");
}

string normalizedEmail = userResponse.Email.ToUpperInvariant();
HutaoUser? user = await this.appDbContext.Users.SingleOrDefaultAsync(u => u.NormalizedUserName == normalizedEmail).ConfigureAwait(false);
TokenResponse tokenResponse;
if (user is null)
{
Passport passport = new()
{
UserName = userResponse.Email,
Password = RandomHelper.GetUpperAndNumberString(16),
};

PassportResult registerResult = await passportService.RegisterAsync(passport, state.DeviceInfo).ConfigureAwait(false);
if (!registerResult.Success)
{
return OAuthResult.Fail(registerResult.Message);
}

tokenResponse = registerResult.Token!;
user = await this.appDbContext.Users.SingleAsync(u => u.NormalizedUserName == normalizedEmail).ConfigureAwait(false);
}
else
{
tokenResponse = await passportService.CreateTokenResponseAsync(user.Id, state.DeviceInfo).ConfigureAwait(false);
}

identity = new()
{
UserId = user.Id,
ProviderKind = OAuthProviderKind.Github,
ProviderId = userResponse.NodeId,
DisplayName = userResponse.Login,
RefreshToken = accessTokenResponse.RefreshToken,
CreatedAt = DateTimeOffset.Now.ToUnixTimeSeconds(),
ExpiresAt = (DateTimeOffset.Now + TimeSpan.FromSeconds(accessTokenResponse.RefreshTokenExpiresIn)).ToUnixTimeSeconds(),
};

await this.appDbContext.OAuthBindIdentities.AddAndSaveAsync(identity).ConfigureAwait(false);
return OAuthResult.LoginSuccess(tokenResponse);
}

return OAuthResult.LoginSuccess(await this.passportService.CreateTokenResponseAsync(identity.UserId, state.DeviceInfo).ConfigureAwait(false));
Expand Down