mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-09 03:08:00 +02:00
Add profiles base
This commit is contained in:
parent
e6927fe6a8
commit
727a3b97a5
@ -36,7 +36,7 @@ public class AuthController : Controller
|
||||
var user = await _manager.Login(req.Username, req.Password);
|
||||
var token = CreateToken(user);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
return new(tokenWriter.WriteToken(token), null);
|
||||
return new(tokenWriter.WriteToken(token), Profile: user.ToPublic());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -59,7 +59,7 @@ public class AuthController : Controller
|
||||
var newUser = await _manager.Register(req.Username, req.Password);
|
||||
var token = CreateToken(newUser);
|
||||
var tokenWriter = new JwtSecurityTokenHandler();
|
||||
return new(tokenWriter.WriteToken(token), null);
|
||||
return new(tokenWriter.WriteToken(token), Profile: newUser.ToPublic());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -74,9 +74,8 @@ public class AuthController : Controller
|
||||
|
||||
var claims = new List<Claim>()
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString()),
|
||||
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(ClaimTypes.Expiration, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString())
|
||||
};
|
||||
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a)));
|
||||
|
||||
@ -102,5 +101,5 @@ public class AuthController : Controller
|
||||
public string Password { get; init; }
|
||||
}
|
||||
|
||||
public record LoginResponse(string? Jwt, string? Error = null);
|
||||
public record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ namespace VoidCat.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ResponseCache(Location = ResponseCacheLocation.Client, Duration = 60)]
|
||||
public async Task<GlobalStats> GetGlobalStats()
|
||||
{
|
||||
var bw = await _statsReporter.GetBandwidth();
|
||||
|
@ -32,12 +32,14 @@ namespace VoidCat.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var uid = HttpContext.GetUserId();
|
||||
var meta = new VoidFileMeta()
|
||||
{
|
||||
MimeType = Request.Headers.GetHeader("V-Content-Type"),
|
||||
Name = Request.Headers.GetHeader("V-Filename"),
|
||||
Description = Request.Headers.GetHeader("V-Description"),
|
||||
Digest = Request.Headers.GetHeader("V-Full-Digest")
|
||||
Digest = Request.Headers.GetHeader("V-Full-Digest"),
|
||||
Uploader = uid
|
||||
};
|
||||
|
||||
var digest = Request.Headers.GetHeader("V-Digest");
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@ -5,6 +7,12 @@ namespace VoidCat.Model;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static Guid? GetUserId(this HttpContext context)
|
||||
{
|
||||
var claimSub = context?.User?.Claims?.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
return Guid.TryParse(claimSub, out var g) ? g : null;
|
||||
}
|
||||
|
||||
public static Guid FromBase58Guid(this string base58)
|
||||
{
|
||||
var enc = new NBitcoin.DataEncoders.Base58Encoder();
|
||||
|
@ -52,6 +52,12 @@ public record VoidFileMeta : IVoidFileMeta
|
||||
/// SHA-256 hash of the file
|
||||
/// </summary>
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// User who uploaded the file
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid? Uploader { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
|
||||
namespace VoidCat.Model;
|
||||
|
||||
@ -20,6 +21,19 @@ public abstract class VoidUser
|
||||
public DateTimeOffset Created { get; init; }
|
||||
|
||||
public DateTimeOffset LastLogin { get; set; }
|
||||
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
public PublicVoidUser ToPublic()
|
||||
{
|
||||
return new(Id, Email)
|
||||
{
|
||||
Roles = Roles,
|
||||
Created = Created,
|
||||
LastLogin = LastLogin,
|
||||
Avatar = Avatar
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PrivateVoidUser : VoidUser
|
||||
|
@ -104,6 +104,7 @@ public class LocalDiskFileStore : IFileStore
|
||||
Description = payload.Meta.Description,
|
||||
Digest = payload.Meta.Digest,
|
||||
MimeType = payload.Meta.MimeType,
|
||||
Uploader = payload.Meta.Uploader,
|
||||
Uploaded = DateTimeOffset.UtcNow,
|
||||
EditSecret = Guid.NewGuid(),
|
||||
Size = total
|
||||
|
@ -3,12 +3,13 @@ import {Link} from "react-router-dom";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {useEffect, useState} from "react";
|
||||
import {FormatBytes} from "../Util";
|
||||
import {AdminApi} from "../Api";
|
||||
import {useApi} from "../Api";
|
||||
import {logout} from "../LoginState";
|
||||
import {PagedSortBy, PageSortOrder} from "../Const";
|
||||
import {PageSelector} from "../PageSelector";
|
||||
|
||||
export function FileList(props) {
|
||||
const {AdminApi} = useApi();
|
||||
const auth = useSelector((state) => state.login.jwt);
|
||||
const dispatch = useDispatch();
|
||||
const [files, setFiles] = useState();
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {useEffect, useState} from "react";
|
||||
import {PagedSortBy, PageSortOrder} from "../Const";
|
||||
import {AdminApi} from "../Api";
|
||||
import {useApi} from "../Api";
|
||||
import {logout} from "../LoginState";
|
||||
import {PageSelector} from "../PageSelector";
|
||||
import moment from "moment";
|
||||
|
||||
export function UserList() {
|
||||
const {AdminApi} = useApi();
|
||||
const auth = useSelector((state) => state.login.jwt);
|
||||
const dispatch = useDispatch();
|
||||
const [users, setUsers] = useState();
|
||||
|
@ -1,33 +1,40 @@
|
||||
async function getJson(method, url, auth, body) {
|
||||
let headers = {
|
||||
"Accept": "application/json"
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
export function useApi() {
|
||||
const auth = useSelector(state => state.login.jwt);
|
||||
|
||||
async function getJson(method, url, body) {
|
||||
let headers = {
|
||||
"Accept": "application/json"
|
||||
};
|
||||
if (auth) {
|
||||
headers["Authorization"] = `Bearer ${auth}`;
|
||||
}
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
AdminApi: {
|
||||
fileList: (pageReq) => getJson("POST", "/admin/file", pageReq),
|
||||
deleteFile: (id) => getJson("DELETE", `/admin/file/${id}`),
|
||||
userList: (pageReq) => getJson("POST", `/admin/user`, pageReq)
|
||||
},
|
||||
Api: {
|
||||
stats: () => getJson("GET", "/stats"),
|
||||
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
||||
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg),
|
||||
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})
|
||||
}
|
||||
};
|
||||
if (auth) {
|
||||
headers["Authorization"] = `Bearer ${auth}`;
|
||||
}
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export const AdminApi = {
|
||||
fileList: (auth, pageReq) => getJson("POST", "/admin/file", auth, pageReq),
|
||||
deleteFile: (auth, id) => getJson("DELETE", `/admin/file/${id}`, auth),
|
||||
userList: (auth, pageReq) => getJson("POST", `/admin/user`, auth, pageReq)
|
||||
}
|
||||
|
||||
export const Api = {
|
||||
stats: () => getJson("GET", "/stats"),
|
||||
fileInfo: (id) => getJson("GET", `/upload/${id}`),
|
||||
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, undefined, cfg),
|
||||
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
|
||||
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
|
||||
login: (username, password) => getJson("POST", `/auth/login`, undefined, {username, password}),
|
||||
register: (username, password) => getJson("POST", `/auth/register`, undefined, {username, password})
|
||||
}
|
@ -1,2 +1,12 @@
|
||||
.app {
|
||||
.page {
|
||||
width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
width: 100vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ import {HomePage} from "./HomePage";
|
||||
import {Admin} from "./Admin/Admin";
|
||||
|
||||
import './App.css';
|
||||
import {UserLogin} from "./UserLogin";
|
||||
import {Profile} from "./Profile";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -14,7 +16,9 @@ function App() {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route exact path="/" element={<HomePage/>}/>
|
||||
<Route path="/admin" element={<Admin/>}/>
|
||||
<Route exact path="/login" element={<UserLogin/>}/>
|
||||
<Route exact path="/u/:id" element={<Profile/>}/>
|
||||
<Route exact path="/admin" element={<Admin/>}/>
|
||||
<Route exact path="/:id" element={<FilePreview/>}/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
@ -2,10 +2,11 @@ import {useState} from "react";
|
||||
|
||||
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
||||
import {NoPaywallConfig} from "./NoPaywallConfig";
|
||||
import {Api} from "./Api";
|
||||
import {useApi} from "./Api";
|
||||
import "./FileEdit.css";
|
||||
|
||||
export function FileEdit(props) {
|
||||
const {Api} = useApi();
|
||||
const file = props.file;
|
||||
const [paywall, setPaywall] = useState(file.paywall?.service);
|
||||
|
||||
|
@ -2,9 +2,10 @@ import {FormatCurrency} from "./Util";
|
||||
import {PaywallServices} from "./Const";
|
||||
import {useState} from "react";
|
||||
import {LightningPaywall} from "./LightningPaywall";
|
||||
import {Api} from "./Api";
|
||||
import {useApi} from "./Api";
|
||||
|
||||
export function FilePaywall(props) {
|
||||
const {Api} = useApi();
|
||||
const file = props.file;
|
||||
const pw = file.paywall;
|
||||
const paywallKey = `paywall-${file.id}`;
|
||||
|
@ -5,11 +5,12 @@ import FeatherIcon from "feather-icons-react";
|
||||
import "./FilePreview.css";
|
||||
import {FileEdit} from "./FileEdit";
|
||||
import {FilePaywall} from "./FilePaywall";
|
||||
import {Api} from "./Api";
|
||||
import {useApi} from "./Api";
|
||||
import {Helmet} from "react-helmet";
|
||||
import {FormatBytes} from "./Util";
|
||||
|
||||
export function FilePreview() {
|
||||
const {Api} = useApi();
|
||||
const params = useParams();
|
||||
const [info, setInfo] = useState();
|
||||
const [order, setOrder] = useState();
|
||||
|
@ -3,6 +3,7 @@ import {buf2hex, ConstName, FormatBytes} from "./Util";
|
||||
import {RateCalculator} from "./RateCalculator";
|
||||
|
||||
import "./FileUpload.css";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
const UploadState = {
|
||||
NotStarted: 0,
|
||||
@ -15,6 +16,7 @@ const UploadState = {
|
||||
};
|
||||
|
||||
export function FileUpload(props) {
|
||||
const auth = useSelector(state => state.login.jwt);
|
||||
const [speed, setSpeed] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [result, setResult] = useState();
|
||||
@ -112,6 +114,9 @@ export function FileUpload(props) {
|
||||
req.setRequestHeader("V-Content-Type", props.file.type);
|
||||
req.setRequestHeader("V-Filename", props.file.name);
|
||||
req.setRequestHeader("V-Digest", buf2hex(digest));
|
||||
if (auth) {
|
||||
req.setRequestHeader("Authorization", `Bearer ${auth}`);
|
||||
}
|
||||
if (typeof (editSecret) === "string") {
|
||||
req.setRequestHeader("V-EditSecret", editSecret);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.footer {
|
||||
margin-top: 10px;
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,21 @@
|
||||
import "./FooterLinks.css"
|
||||
import StrikeLogo from "./image/strike.png";
|
||||
import {Link} from "react-router-dom";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
export function FooterLinks(props){
|
||||
export function FooterLinks(){
|
||||
const auth = useSelector(state => state.login.jwt);
|
||||
const profile = useSelector(state => state.login.profile);
|
||||
|
||||
return (
|
||||
<div className="footer">
|
||||
<a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a>
|
||||
<a href="https://invite.strike.me/KS0FYF" target="_blank">Get Strike <img src={StrikeLogo} alt="Strike logo"/> </a>
|
||||
<a href="https://github.com/v0l/void.cat" target="_blank">GitHub</a>
|
||||
{!auth ?
|
||||
<Link to={"/login"}>Login</Link> :
|
||||
<Link to={`/u/${profile?.id}`}>Profile</Link>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,9 +3,10 @@ import FeatherIcon from "feather-icons-react";
|
||||
import {FormatBytes} from "./Util";
|
||||
|
||||
import "./GlobalStats.css";
|
||||
import {Api} from "./Api";
|
||||
import {useApi} from "./Api";
|
||||
|
||||
export function GlobalStats(props) {
|
||||
const {Api} = useApi();
|
||||
let [stats, setStats] = useState();
|
||||
|
||||
async function loadStats() {
|
||||
|
@ -1,12 +0,0 @@
|
||||
.home {
|
||||
width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.home {
|
||||
width: 100vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
@ -2,11 +2,9 @@
|
||||
import {GlobalStats} from "./GlobalStats";
|
||||
import {FooterLinks} from "./FooterLinks";
|
||||
|
||||
import "./HomePage.css";
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div className="home">
|
||||
<div className="page">
|
||||
<Dropzone/>
|
||||
<GlobalStats/>
|
||||
<FooterLinks/>
|
||||
|
@ -3,9 +3,10 @@ import {useEffect} from "react";
|
||||
|
||||
import {Countdown} from "./Countdown";
|
||||
import {PaywallOrderState} from "./Const";
|
||||
import {Api} from "./Api";
|
||||
import {useApi} from "./Api";
|
||||
|
||||
export function LightningPaywall(props) {
|
||||
const {Api} = useApi();
|
||||
const file = props.file;
|
||||
const order = props.order;
|
||||
const onPaid = props.onPaid;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {useState} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {setAuth} from "./LoginState";
|
||||
|
||||
import {useApi} from "./Api";
|
||||
import "./Login.css";
|
||||
import {Api} from "./Api";
|
||||
|
||||
export function Login() {
|
||||
const {Api} = useApi();
|
||||
const [username, setUsername] = useState();
|
||||
const [password, setPassword] = useState();
|
||||
const [error, setError] = useState();
|
||||
@ -19,12 +19,12 @@ export function Login() {
|
||||
if (req.ok) {
|
||||
let rsp = await req.json();
|
||||
if (rsp.jwt) {
|
||||
dispatch(setAuth(rsp.jwt));
|
||||
dispatch(setAuth(rsp));
|
||||
} else {
|
||||
setError(rsp.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
e.target.disabled = false;
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,25 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
const LocalStorageKey = "token";
|
||||
const LocalStorageProfileKey = "profile";
|
||||
|
||||
export const LoginState = createSlice({
|
||||
name: "Login",
|
||||
initialState: {
|
||||
jwt: window.localStorage.getItem(LocalStorageKey)
|
||||
jwt: window.localStorage.getItem(LocalStorageKey),
|
||||
profile: JSON.parse(window.localStorage.getItem(LocalStorageProfileKey))
|
||||
},
|
||||
reducers: {
|
||||
setAuth: (state, action) => {
|
||||
state.jwt = action.payload;
|
||||
state.jwt = action.payload.jwt;
|
||||
state.profile = action.payload.profile;
|
||||
window.localStorage.setItem(LocalStorageKey, state.jwt);
|
||||
window.localStorage.setItem(LocalStorageProfileKey, JSON.stringify(state.profile));
|
||||
},
|
||||
logout: (state) => {
|
||||
state.jwt = null;
|
||||
window.localStorage.removeItem(LocalStorageKey);
|
||||
window.localStorage.removeItem(LocalStorageProfileKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
5
VoidCat/spa/src/Profile.js
Normal file
5
VoidCat/spa/src/Profile.js
Normal file
@ -0,0 +1,5 @@
|
||||
export function Profile() {
|
||||
return (
|
||||
<h1>Coming soon..</h1>
|
||||
);
|
||||
}
|
21
VoidCat/spa/src/UserLogin.js
Normal file
21
VoidCat/spa/src/UserLogin.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {Login} from "./Login";
|
||||
import {useSelector} from "react-redux";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useEffect} from "react";
|
||||
|
||||
export function UserLogin() {
|
||||
const auth = useSelector((state) => state.login.jwt);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if(auth){
|
||||
navigate("/");
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Login/>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user