mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-09 03:08:00 +02:00
Add hCaptcha to login page
This commit is contained in:
parent
8b546c6437
commit
80880a18a8
@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using VoidCat.Model;
|
||||
@ -15,11 +14,13 @@ public class AuthController : Controller
|
||||
{
|
||||
private readonly IUserManager _manager;
|
||||
private readonly VoidSettings _settings;
|
||||
private readonly ICaptchaVerifier _captchaVerifier;
|
||||
|
||||
public AuthController(IUserManager userStore, VoidSettings settings)
|
||||
public AuthController(IUserManager userStore, VoidSettings settings, ICaptchaVerifier captchaVerifier)
|
||||
{
|
||||
_manager = userStore;
|
||||
_settings = settings;
|
||||
_captchaVerifier = captchaVerifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -39,6 +40,12 @@ public class AuthController : Controller
|
||||
return new(null, error);
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if (!await _captchaVerifier.Verify(req.Captcha))
|
||||
{
|
||||
return new(null, "Captcha verification failed");
|
||||
}
|
||||
|
||||
var user = await _manager.Login(req.Username, req.Password);
|
||||
var token = CreateToken(user);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
@ -67,6 +74,12 @@ public class AuthController : Controller
|
||||
return new(null, error);
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if (!await _captchaVerifier.Verify(req.Captcha))
|
||||
{
|
||||
return new(null, "Captcha verification failed");
|
||||
}
|
||||
|
||||
var newUser = await _manager.Register(req.Username, req.Password);
|
||||
var token = CreateToken(newUser);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
@ -96,7 +109,7 @@ public class AuthController : Controller
|
||||
}
|
||||
|
||||
|
||||
public class LoginRequest
|
||||
public sealed class LoginRequest
|
||||
{
|
||||
public LoginRequest(string username, string password)
|
||||
{
|
||||
@ -106,12 +119,14 @@ public class AuthController : Controller
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Username { get; init; }
|
||||
public string Username { get; }
|
||||
|
||||
[Required]
|
||||
[MinLength(6)]
|
||||
public string Password { get; init; }
|
||||
public string Password { get; }
|
||||
|
||||
public string? Captcha { get; init; }
|
||||
}
|
||||
|
||||
public record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null);
|
||||
public sealed record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null);
|
||||
}
|
||||
|
44
VoidCat/Controllers/InfoController.cs
Normal file
44
VoidCat/Controllers/InfoController.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Controllers;
|
||||
|
||||
[Route("info")]
|
||||
public class InfoController : Controller
|
||||
{
|
||||
private readonly IStatsReporter _statsReporter;
|
||||
private readonly IFileStore _fileStore;
|
||||
private readonly VoidSettings _settings;
|
||||
|
||||
public InfoController(IStatsReporter statsReporter, IFileStore fileStore, VoidSettings settings)
|
||||
{
|
||||
_statsReporter = statsReporter;
|
||||
_fileStore = fileStore;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return system info
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
|
||||
public async Task<GlobalInfo> GetGlobalStats()
|
||||
{
|
||||
var bw = await _statsReporter.GetBandwidth();
|
||||
var bytes = 0UL;
|
||||
var count = 0;
|
||||
var files = await _fileStore.ListFiles(new(0, Int32.MaxValue));
|
||||
await foreach (var vf in files.Results)
|
||||
{
|
||||
bytes += vf.Metadata?.Size ?? 0;
|
||||
count++;
|
||||
}
|
||||
|
||||
return new(bw, bytes, count, BuildInfo.GetBuildInfo(), _settings.CaptchaSettings?.SiteKey);
|
||||
}
|
||||
|
||||
public sealed record GlobalInfo(Bandwidth Bandwidth, ulong TotalBytes, int Count, BuildInfo BuildInfo,
|
||||
string? CaptchaSiteKey);
|
||||
}
|
@ -16,28 +16,6 @@ namespace VoidCat.Controllers
|
||||
_fileStore = fileStore;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return system info
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
|
||||
public async Task<GlobalStats> GetGlobalStats()
|
||||
{
|
||||
var bw = await _statsReporter.GetBandwidth();
|
||||
var bytes = 0UL;
|
||||
var count = 0;
|
||||
var files = await _fileStore.ListFiles(new(0, Int32.MaxValue));
|
||||
await foreach (var vf in files.Results)
|
||||
{
|
||||
bytes += vf.Metadata?.Size ?? 0;
|
||||
count++;
|
||||
}
|
||||
|
||||
return new(bw, bytes, count, BuildInfo.GetBuildInfo());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get stats for a specific file
|
||||
/// </summary>
|
||||
@ -52,7 +30,5 @@ namespace VoidCat.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record GlobalStats(Bandwidth Bandwidth, ulong TotalBytes, int Count, BuildInfo BuildInfo);
|
||||
|
||||
public sealed record FileStats(Bandwidth Bandwidth);
|
||||
}
|
@ -25,6 +25,10 @@ namespace VoidCat.Model
|
||||
public CloudStorageSettings? CloudStorage { get; init; }
|
||||
|
||||
public VirusScannerSettings? VirusScanner { get; init; }
|
||||
|
||||
public IEnumerable<string>? RequestHeadersLog { get; init; }
|
||||
|
||||
public CaptchaSettings? CaptchaSettings { get; init; }
|
||||
}
|
||||
|
||||
public sealed class TorSettings
|
||||
@ -78,4 +82,10 @@ namespace VoidCat.Model
|
||||
{
|
||||
public string? ApiKey { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CaptchaSettings
|
||||
{
|
||||
public string? SiteKey { get; init; }
|
||||
public string? Secret { get; init; }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using VoidCat.Model;
|
||||
using VoidCat.Services;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Background;
|
||||
using VoidCat.Services.Captcha;
|
||||
using VoidCat.Services.Files;
|
||||
using VoidCat.Services.InMemory;
|
||||
using VoidCat.Services.Migrations;
|
||||
@ -52,10 +53,17 @@ if (useRedis)
|
||||
|
||||
services.AddHttpLogging((o) =>
|
||||
{
|
||||
o.LoggingFields = HttpLoggingFields.RequestHeaders | HttpLoggingFields.ResponseHeaders | HttpLoggingFields.Response;
|
||||
o.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.ResponsePropertiesAndHeaders;
|
||||
o.RequestBodyLogLimit = 4096;
|
||||
o.ResponseBodyLogLimit = 4096;
|
||||
|
||||
o.MediaTypeOptions.Clear();
|
||||
o.MediaTypeOptions.AddText("application/json");
|
||||
|
||||
foreach (var h in voidSettings.RequestHeadersLog ?? Enumerable.Empty<string>())
|
||||
{
|
||||
o.RequestHeaders.Add(h);
|
||||
}
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.AddSwaggerGen(c =>
|
||||
@ -143,6 +151,9 @@ services.AddHostedService<DeleteUnverifiedAccounts>();
|
||||
// virus scanner
|
||||
services.AddVirusScanner(voidSettings);
|
||||
|
||||
// captcha
|
||||
services.AddCaptcha(voidSettings);
|
||||
|
||||
if (useRedis)
|
||||
{
|
||||
services.AddTransient<ICache, RedisCache>();
|
||||
@ -172,7 +183,7 @@ foreach (var migration in migrations)
|
||||
app.UseStaticFiles();
|
||||
#endif
|
||||
|
||||
app.UseHttpLogging();
|
||||
app.UseHttpLogging();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseSwagger();
|
||||
|
6
VoidCat/Services/Abstractions/ICaptchaVerifier.cs
Normal file
6
VoidCat/Services/Abstractions/ICaptchaVerifier.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface ICaptchaVerifier
|
||||
{
|
||||
ValueTask<bool> Verify(string? token);
|
||||
}
|
19
VoidCat/Services/Captcha/CaptchaStartup.cs
Normal file
19
VoidCat/Services/Captcha/CaptchaStartup.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Captcha;
|
||||
|
||||
public static class CaptchaStartup
|
||||
{
|
||||
public static void AddCaptcha(this IServiceCollection services, VoidSettings settings)
|
||||
{
|
||||
if (settings.CaptchaSettings != default)
|
||||
{
|
||||
services.AddTransient<ICaptchaVerifier, hCaptchaVerifier>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<ICaptchaVerifier, NoOpVerifier>();
|
||||
}
|
||||
}
|
||||
}
|
11
VoidCat/Services/Captcha/NoOpVerifier.cs
Normal file
11
VoidCat/Services/Captcha/NoOpVerifier.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Captcha;
|
||||
|
||||
public class NoOpVerifier : ICaptchaVerifier
|
||||
{
|
||||
public ValueTask<bool> Verify(string? token)
|
||||
{
|
||||
return ValueTask.FromResult(token == null);
|
||||
}
|
||||
}
|
45
VoidCat/Services/Captcha/hCaptchaVerifier.cs
Normal file
45
VoidCat/Services/Captcha/hCaptchaVerifier.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Captcha;
|
||||
|
||||
public class hCaptchaVerifier : ICaptchaVerifier
|
||||
{
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
private readonly VoidSettings _settings;
|
||||
|
||||
public hCaptchaVerifier(IHttpClientFactory clientFactory, VoidSettings settings)
|
||||
{
|
||||
_clientFactory = clientFactory;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> Verify(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token)) return false;
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, "https://hcaptcha.com/siteverify");
|
||||
req.Content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("response", token),
|
||||
new KeyValuePair<string, string>("secret", _settings.CaptchaSettings!.Secret!)
|
||||
});
|
||||
|
||||
using var cli = _clientFactory.CreateClient();
|
||||
var rsp = await cli.SendAsync(req);
|
||||
if (rsp.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var body = JsonConvert.DeserializeObject<hCaptchaResponse>(await rsp.Content.ReadAsStringAsync());
|
||||
return body?.Success == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal sealed class hCaptchaResponse
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"proxy": "https://localhost:7195",
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.1.1",
|
||||
"@reduxjs/toolkit": "^1.7.2",
|
||||
"feather-icons-react": "^0.5.0",
|
||||
"moment": "^2.29.1",
|
||||
|
@ -30,13 +30,13 @@ export function useApi() {
|
||||
userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth)
|
||||
},
|
||||
Api: {
|
||||
stats: () => getJson("GET", "/stats"),
|
||||
info: () => getJson("GET", "/info"),
|
||||
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
||||
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth),
|
||||
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||
login: (username, password) => getJson("POST", `/auth/login`, {username, password}),
|
||||
register: (username, password) => getJson("POST", `/auth/register`, {username, password}),
|
||||
login: (username, password, captcha) => getJson("POST", `/auth/login`, {username, password, captcha}),
|
||||
register: (username, password, captcha) => getJson("POST", `/auth/register`, {username, password, captcha}),
|
||||
getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth),
|
||||
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
||||
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
||||
|
@ -5,20 +5,12 @@ import {FormatBytes} from "./Util";
|
||||
import "./GlobalStats.css";
|
||||
import {useApi} from "./Api";
|
||||
import moment from "moment";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
export function GlobalStats(props) {
|
||||
const {Api} = useApi();
|
||||
let [stats, setStats] = useState();
|
||||
|
||||
async function loadStats() {
|
||||
let req = await Api.stats();
|
||||
if (req.ok) {
|
||||
setStats(await req.json());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => loadStats(), []);
|
||||
|
||||
let stats = useSelector(state => state.info.stats);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<dl className="stats">
|
||||
|
@ -5,6 +5,7 @@ import {InlineProfile} from "./InlineProfile";
|
||||
import {useApi} from "./Api";
|
||||
import {logout, setProfile} from "./LoginState";
|
||||
import {useEffect} from "react";
|
||||
import {setStats} from "./SiteInfoStore";
|
||||
|
||||
export function Header() {
|
||||
const dispatch = useDispatch();
|
||||
@ -22,9 +23,16 @@ export function Header() {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function loadStats() {
|
||||
let req = await Api.info();
|
||||
if (req.ok) {
|
||||
dispatch(setStats(await req.json()));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initProfile();
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -1,22 +1,25 @@
|
||||
import {useState} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {setAuth} from "./LoginState";
|
||||
import {useApi} from "./Api";
|
||||
import "./Login.css";
|
||||
import {btnDisable, btnEnable} from "./Util";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
export function Login() {
|
||||
const {Api} = useApi();
|
||||
const [username, setUsername] = useState();
|
||||
const [password, setPassword] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [captchaResponse, setCaptchaResponse] = useState();
|
||||
const captchaKey = useSelector(state => state.info.stats.captchaSiteKey);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function login(e, fnLogin) {
|
||||
if(!btnDisable(e.target)) return;
|
||||
setError(null);
|
||||
|
||||
let req = await fnLogin(username, password);
|
||||
let req = await fnLogin(username, password, captchaResponse);
|
||||
if (req.ok) {
|
||||
let rsp = await req.json();
|
||||
if (rsp.jwt) {
|
||||
@ -38,6 +41,7 @@ export function Login() {
|
||||
<dt>Password:</dt>
|
||||
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
||||
</dl>
|
||||
{captchaKey ? <HCaptcha sitekey={captchaKey} onVerify={setCaptchaResponse}/> : null}
|
||||
<div className="btn" onClick={(e) => login(e, Api.login)}>Login</div>
|
||||
<div className="btn" onClick={(e) => login(e, Api.register)}>Register</div>
|
||||
{error ? <div className="error-msg">{error}</div> : null}
|
||||
|
16
VoidCat/spa/src/SiteInfoStore.js
Normal file
16
VoidCat/spa/src/SiteInfoStore.js
Normal file
@ -0,0 +1,16 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
export const SiteInfoState = createSlice({
|
||||
name: "SiteInfo",
|
||||
initialState: {
|
||||
stats: null
|
||||
},
|
||||
reducers: {
|
||||
setStats: (state, action) => {
|
||||
state.stats = action.payload;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const {setStats} = SiteInfoState.actions;
|
||||
export default SiteInfoState.reducer;
|
@ -1,8 +1,10 @@
|
||||
import {configureStore} from "@reduxjs/toolkit";
|
||||
import loginReducer from "./LoginState";
|
||||
import siteInfoReducer from "./SiteInfoStore";
|
||||
|
||||
export default configureStore({
|
||||
reducer: {
|
||||
login: loginReducer
|
||||
login: loginReducer,
|
||||
info: siteInfoReducer
|
||||
}
|
||||
});
|
@ -1087,6 +1087,11 @@
|
||||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@hcaptcha/react-hcaptcha@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.1.1.tgz#b7f9fef825fe52595e38b61f125dc8d87d2a5440"
|
||||
integrity sha512-fydc0ob5NOEYiJPmohOmPqoBjLPhUokvMZBf1hOhrVcpMyZJbc1N/HPV4kysSayC34IQsaBRtEpswQFbHwe54w==
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
|
||||
|
Loading…
x
Reference in New Issue
Block a user