mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-09-27 12:46:31 +02:00
Add hCaptcha to login page
This commit is contained in:
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Cors;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
@@ -15,11 +14,13 @@ public class AuthController : Controller
|
|||||||
{
|
{
|
||||||
private readonly IUserManager _manager;
|
private readonly IUserManager _manager;
|
||||||
private readonly VoidSettings _settings;
|
private readonly VoidSettings _settings;
|
||||||
|
private readonly ICaptchaVerifier _captchaVerifier;
|
||||||
|
|
||||||
public AuthController(IUserManager userStore, VoidSettings settings)
|
public AuthController(IUserManager userStore, VoidSettings settings, ICaptchaVerifier captchaVerifier)
|
||||||
{
|
{
|
||||||
_manager = userStore;
|
_manager = userStore;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_captchaVerifier = captchaVerifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -39,6 +40,12 @@ public class AuthController : Controller
|
|||||||
return new(null, error);
|
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 user = await _manager.Login(req.Username, req.Password);
|
||||||
var token = CreateToken(user);
|
var token = CreateToken(user);
|
||||||
var tokenWriter = new JwtSecurityTokenHandler();
|
var tokenWriter = new JwtSecurityTokenHandler();
|
||||||
@@ -67,6 +74,12 @@ public class AuthController : Controller
|
|||||||
return new(null, error);
|
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 newUser = await _manager.Register(req.Username, req.Password);
|
||||||
var token = CreateToken(newUser);
|
var token = CreateToken(newUser);
|
||||||
var tokenWriter = new JwtSecurityTokenHandler();
|
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)
|
public LoginRequest(string username, string password)
|
||||||
{
|
{
|
||||||
@@ -106,12 +119,14 @@ public class AuthController : Controller
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string Username { get; init; }
|
public string Username { get; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[MinLength(6)]
|
[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;
|
_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>
|
/// <summary>
|
||||||
/// Get stats for a specific file
|
/// Get stats for a specific file
|
||||||
/// </summary>
|
/// </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);
|
public sealed record FileStats(Bandwidth Bandwidth);
|
||||||
}
|
}
|
@@ -25,6 +25,10 @@ namespace VoidCat.Model
|
|||||||
public CloudStorageSettings? CloudStorage { get; init; }
|
public CloudStorageSettings? CloudStorage { get; init; }
|
||||||
|
|
||||||
public VirusScannerSettings? VirusScanner { get; init; }
|
public VirusScannerSettings? VirusScanner { get; init; }
|
||||||
|
|
||||||
|
public IEnumerable<string>? RequestHeadersLog { get; init; }
|
||||||
|
|
||||||
|
public CaptchaSettings? CaptchaSettings { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TorSettings
|
public sealed class TorSettings
|
||||||
@@ -78,4 +82,10 @@ namespace VoidCat.Model
|
|||||||
{
|
{
|
||||||
public string? ApiKey { get; init; }
|
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;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Background;
|
using VoidCat.Services.Background;
|
||||||
|
using VoidCat.Services.Captcha;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
using VoidCat.Services.InMemory;
|
using VoidCat.Services.InMemory;
|
||||||
using VoidCat.Services.Migrations;
|
using VoidCat.Services.Migrations;
|
||||||
@@ -52,10 +53,17 @@ if (useRedis)
|
|||||||
|
|
||||||
services.AddHttpLogging((o) =>
|
services.AddHttpLogging((o) =>
|
||||||
{
|
{
|
||||||
o.LoggingFields = HttpLoggingFields.RequestHeaders | HttpLoggingFields.ResponseHeaders | HttpLoggingFields.Response;
|
o.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.ResponsePropertiesAndHeaders;
|
||||||
o.RequestBodyLogLimit = 4096;
|
o.RequestBodyLogLimit = 4096;
|
||||||
o.ResponseBodyLogLimit = 4096;
|
o.ResponseBodyLogLimit = 4096;
|
||||||
|
|
||||||
|
o.MediaTypeOptions.Clear();
|
||||||
o.MediaTypeOptions.AddText("application/json");
|
o.MediaTypeOptions.AddText("application/json");
|
||||||
|
|
||||||
|
foreach (var h in voidSettings.RequestHeadersLog ?? Enumerable.Empty<string>())
|
||||||
|
{
|
||||||
|
o.RequestHeaders.Add(h);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
services.AddSwaggerGen(c =>
|
services.AddSwaggerGen(c =>
|
||||||
@@ -143,6 +151,9 @@ services.AddHostedService<DeleteUnverifiedAccounts>();
|
|||||||
// virus scanner
|
// virus scanner
|
||||||
services.AddVirusScanner(voidSettings);
|
services.AddVirusScanner(voidSettings);
|
||||||
|
|
||||||
|
// captcha
|
||||||
|
services.AddCaptcha(voidSettings);
|
||||||
|
|
||||||
if (useRedis)
|
if (useRedis)
|
||||||
{
|
{
|
||||||
services.AddTransient<ICache, RedisCache>();
|
services.AddTransient<ICache, RedisCache>();
|
||||||
@@ -172,7 +183,7 @@ foreach (var migration in migrations)
|
|||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
app.UseHttpLogging();
|
app.UseHttpLogging();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseSwagger();
|
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,
|
"private": true,
|
||||||
"proxy": "https://localhost:7195",
|
"proxy": "https://localhost:7195",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.1.1",
|
||||||
"@reduxjs/toolkit": "^1.7.2",
|
"@reduxjs/toolkit": "^1.7.2",
|
||||||
"feather-icons-react": "^0.5.0",
|
"feather-icons-react": "^0.5.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
@@ -30,13 +30,13 @@ export function useApi() {
|
|||||||
userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth)
|
userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth)
|
||||||
},
|
},
|
||||||
Api: {
|
Api: {
|
||||||
stats: () => getJson("GET", "/stats"),
|
info: () => getJson("GET", "/info"),
|
||||||
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
||||||
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth),
|
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth),
|
||||||
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||||
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||||
login: (username, password) => getJson("POST", `/auth/login`, {username, password}),
|
login: (username, password, captcha) => getJson("POST", `/auth/login`, {username, password, captcha}),
|
||||||
register: (username, password) => getJson("POST", `/auth/register`, {username, password}),
|
register: (username, password, captcha) => getJson("POST", `/auth/register`, {username, password, captcha}),
|
||||||
getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth),
|
getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth),
|
||||||
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
||||||
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
||||||
|
@@ -5,20 +5,12 @@ import {FormatBytes} from "./Util";
|
|||||||
import "./GlobalStats.css";
|
import "./GlobalStats.css";
|
||||||
import {useApi} from "./Api";
|
import {useApi} from "./Api";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import {useSelector} from "react-redux";
|
||||||
|
|
||||||
export function GlobalStats(props) {
|
export function GlobalStats(props) {
|
||||||
const {Api} = useApi();
|
const {Api} = useApi();
|
||||||
let [stats, setStats] = useState();
|
let stats = useSelector(state => state.info.stats);
|
||||||
|
|
||||||
async function loadStats() {
|
|
||||||
let req = await Api.stats();
|
|
||||||
if (req.ok) {
|
|
||||||
setStats(await req.json());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => loadStats(), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<dl className="stats">
|
<dl className="stats">
|
||||||
|
@@ -5,6 +5,7 @@ import {InlineProfile} from "./InlineProfile";
|
|||||||
import {useApi} from "./Api";
|
import {useApi} from "./Api";
|
||||||
import {logout, setProfile} from "./LoginState";
|
import {logout, setProfile} from "./LoginState";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
|
import {setStats} from "./SiteInfoStore";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const dispatch = useDispatch();
|
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(() => {
|
useEffect(() => {
|
||||||
initProfile();
|
initProfile();
|
||||||
|
loadStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -1,22 +1,25 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useDispatch} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {setAuth} from "./LoginState";
|
import {setAuth} from "./LoginState";
|
||||||
import {useApi} from "./Api";
|
import {useApi} from "./Api";
|
||||||
import "./Login.css";
|
import "./Login.css";
|
||||||
import {btnDisable, btnEnable} from "./Util";
|
import {btnDisable, btnEnable} from "./Util";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const {Api} = useApi();
|
const {Api} = useApi();
|
||||||
const [username, setUsername] = useState();
|
const [username, setUsername] = useState();
|
||||||
const [password, setPassword] = useState();
|
const [password, setPassword] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
|
const [captchaResponse, setCaptchaResponse] = useState();
|
||||||
|
const captchaKey = useSelector(state => state.info.stats.captchaSiteKey);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
async function login(e, fnLogin) {
|
async function login(e, fnLogin) {
|
||||||
if(!btnDisable(e.target)) return;
|
if(!btnDisable(e.target)) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
let req = await fnLogin(username, password);
|
let req = await fnLogin(username, password, captchaResponse);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
let rsp = await req.json();
|
let rsp = await req.json();
|
||||||
if (rsp.jwt) {
|
if (rsp.jwt) {
|
||||||
@@ -38,6 +41,7 @@ export function Login() {
|
|||||||
<dt>Password:</dt>
|
<dt>Password:</dt>
|
||||||
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
<dd><input type="password" onChange={(e) => setPassword(e.target.value)}/></dd>
|
||||||
</dl>
|
</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.login)}>Login</div>
|
||||||
<div className="btn" onClick={(e) => login(e, Api.register)}>Register</div>
|
<div className="btn" onClick={(e) => login(e, Api.register)}>Register</div>
|
||||||
{error ? <div className="error-msg">{error}</div> : null}
|
{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 {configureStore} from "@reduxjs/toolkit";
|
||||||
import loginReducer from "./LoginState";
|
import loginReducer from "./LoginState";
|
||||||
|
import siteInfoReducer from "./SiteInfoStore";
|
||||||
|
|
||||||
export default configureStore({
|
export default configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
login: loginReducer
|
login: loginReducer,
|
||||||
|
info: siteInfoReducer
|
||||||
}
|
}
|
||||||
});
|
});
|
@@ -1087,6 +1087,11 @@
|
|||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
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":
|
"@humanwhocodes/config-array@^0.9.2":
|
||||||
version "0.9.2"
|
version "0.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914"
|
||||||
|
Reference in New Issue
Block a user