Add profiles base

This commit is contained in:
Kieran 2022-02-24 23:05:33 +00:00
parent e6927fe6a8
commit 727a3b97a5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
26 changed files with 160 additions and 70 deletions

View File

@ -36,7 +36,7 @@ public class AuthController : Controller
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();
return new(tokenWriter.WriteToken(token), null); return new(tokenWriter.WriteToken(token), Profile: user.ToPublic());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -59,7 +59,7 @@ public class AuthController : Controller
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();
return new(tokenWriter.WriteToken(token), null); return new(tokenWriter.WriteToken(token), Profile: newUser.ToPublic());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -74,9 +74,8 @@ public class AuthController : Controller
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString()), new(ClaimTypes.Expiration, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString())
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
}; };
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a))); 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 string Password { get; init; }
} }
public record LoginResponse(string? Jwt, string? Error = null); public record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null);
} }

View File

@ -19,6 +19,7 @@ namespace VoidCat.Controllers
} }
[HttpGet] [HttpGet]
[ResponseCache(Location = ResponseCacheLocation.Client, Duration = 60)]
public async Task<GlobalStats> GetGlobalStats() public async Task<GlobalStats> GetGlobalStats()
{ {
var bw = await _statsReporter.GetBandwidth(); var bw = await _statsReporter.GetBandwidth();

View File

@ -32,12 +32,14 @@ namespace VoidCat.Controllers
{ {
try try
{ {
var uid = HttpContext.GetUserId();
var meta = new VoidFileMeta() var meta = new VoidFileMeta()
{ {
MimeType = Request.Headers.GetHeader("V-Content-Type"), MimeType = Request.Headers.GetHeader("V-Content-Type"),
Name = Request.Headers.GetHeader("V-Filename"), Name = Request.Headers.GetHeader("V-Filename"),
Description = Request.Headers.GetHeader("V-Description"), 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"); var digest = Request.Headers.GetHeader("V-Digest");

View File

@ -1,3 +1,5 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@ -5,6 +7,12 @@ namespace VoidCat.Model;
public static class Extensions 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) public static Guid FromBase58Guid(this string base58)
{ {
var enc = new NBitcoin.DataEncoders.Base58Encoder(); var enc = new NBitcoin.DataEncoders.Base58Encoder();

View File

@ -52,6 +52,12 @@ public record VoidFileMeta : IVoidFileMeta
/// SHA-256 hash of the file /// SHA-256 hash of the file
/// </summary> /// </summary>
public string? Digest { get; init; } public string? Digest { get; init; }
/// <summary>
/// User who uploaded the file
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid? Uploader { get; init; }
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using VoidCat.Model;
namespace VoidCat.Model; namespace VoidCat.Model;
@ -20,6 +21,19 @@ public abstract class VoidUser
public DateTimeOffset Created { get; init; } public DateTimeOffset Created { get; init; }
public DateTimeOffset LastLogin { get; set; } 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 public sealed class PrivateVoidUser : VoidUser

View File

@ -104,6 +104,7 @@ public class LocalDiskFileStore : IFileStore
Description = payload.Meta.Description, Description = payload.Meta.Description,
Digest = payload.Meta.Digest, Digest = payload.Meta.Digest,
MimeType = payload.Meta.MimeType, MimeType = payload.Meta.MimeType,
Uploader = payload.Meta.Uploader,
Uploaded = DateTimeOffset.UtcNow, Uploaded = DateTimeOffset.UtcNow,
EditSecret = Guid.NewGuid(), EditSecret = Guid.NewGuid(),
Size = total Size = total

View File

@ -3,12 +3,13 @@ import {Link} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {FormatBytes} from "../Util"; import {FormatBytes} from "../Util";
import {AdminApi} from "../Api"; import {useApi} from "../Api";
import {logout} from "../LoginState"; import {logout} from "../LoginState";
import {PagedSortBy, PageSortOrder} from "../Const"; import {PagedSortBy, PageSortOrder} from "../Const";
import {PageSelector} from "../PageSelector"; import {PageSelector} from "../PageSelector";
export function FileList(props) { export function FileList(props) {
const {AdminApi} = useApi();
const auth = useSelector((state) => state.login.jwt); const auth = useSelector((state) => state.login.jwt);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [files, setFiles] = useState(); const [files, setFiles] = useState();

View File

@ -1,12 +1,13 @@
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {PagedSortBy, PageSortOrder} from "../Const"; import {PagedSortBy, PageSortOrder} from "../Const";
import {AdminApi} from "../Api"; import {useApi} from "../Api";
import {logout} from "../LoginState"; import {logout} from "../LoginState";
import {PageSelector} from "../PageSelector"; import {PageSelector} from "../PageSelector";
import moment from "moment"; import moment from "moment";
export function UserList() { export function UserList() {
const {AdminApi} = useApi();
const auth = useSelector((state) => state.login.jwt); const auth = useSelector((state) => state.login.jwt);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [users, setUsers] = useState(); const [users, setUsers] = useState();

View File

@ -1,33 +1,40 @@
async function getJson(method, url, auth, body) { import {useSelector} from "react-redux";
let headers = {
"Accept": "application/json" 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})
} }

View File

@ -1,2 +1,12 @@
.app { .page {
width: 720px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 720px) {
.page {
width: 100vw;
white-space: nowrap;
}
} }

View File

@ -6,6 +6,8 @@ import {HomePage} from "./HomePage";
import {Admin} from "./Admin/Admin"; import {Admin} from "./Admin/Admin";
import './App.css'; import './App.css';
import {UserLogin} from "./UserLogin";
import {Profile} from "./Profile";
function App() { function App() {
return ( return (
@ -14,7 +16,9 @@ function App() {
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route exact path="/" element={<HomePage/>}/> <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/>}/> <Route exact path="/:id" element={<FilePreview/>}/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@ -2,10 +2,11 @@ import {useState} from "react";
import {StrikePaywallConfig} from "./StrikePaywallConfig"; import {StrikePaywallConfig} from "./StrikePaywallConfig";
import {NoPaywallConfig} from "./NoPaywallConfig"; import {NoPaywallConfig} from "./NoPaywallConfig";
import {Api} from "./Api"; import {useApi} from "./Api";
import "./FileEdit.css"; import "./FileEdit.css";
export function FileEdit(props) { export function FileEdit(props) {
const {Api} = useApi();
const file = props.file; const file = props.file;
const [paywall, setPaywall] = useState(file.paywall?.service); const [paywall, setPaywall] = useState(file.paywall?.service);

View File

@ -2,9 +2,10 @@ import {FormatCurrency} from "./Util";
import {PaywallServices} from "./Const"; import {PaywallServices} from "./Const";
import {useState} from "react"; import {useState} from "react";
import {LightningPaywall} from "./LightningPaywall"; import {LightningPaywall} from "./LightningPaywall";
import {Api} from "./Api"; import {useApi} from "./Api";
export function FilePaywall(props) { export function FilePaywall(props) {
const {Api} = useApi();
const file = props.file; const file = props.file;
const pw = file.paywall; const pw = file.paywall;
const paywallKey = `paywall-${file.id}`; const paywallKey = `paywall-${file.id}`;

View File

@ -5,11 +5,12 @@ import FeatherIcon from "feather-icons-react";
import "./FilePreview.css"; import "./FilePreview.css";
import {FileEdit} from "./FileEdit"; import {FileEdit} from "./FileEdit";
import {FilePaywall} from "./FilePaywall"; import {FilePaywall} from "./FilePaywall";
import {Api} from "./Api"; import {useApi} from "./Api";
import {Helmet} from "react-helmet"; import {Helmet} from "react-helmet";
import {FormatBytes} from "./Util"; import {FormatBytes} from "./Util";
export function FilePreview() { export function FilePreview() {
const {Api} = useApi();
const params = useParams(); const params = useParams();
const [info, setInfo] = useState(); const [info, setInfo] = useState();
const [order, setOrder] = useState(); const [order, setOrder] = useState();

View File

@ -3,6 +3,7 @@ import {buf2hex, ConstName, FormatBytes} from "./Util";
import {RateCalculator} from "./RateCalculator"; import {RateCalculator} from "./RateCalculator";
import "./FileUpload.css"; import "./FileUpload.css";
import {useSelector} from "react-redux";
const UploadState = { const UploadState = {
NotStarted: 0, NotStarted: 0,
@ -15,6 +16,7 @@ const UploadState = {
}; };
export function FileUpload(props) { export function FileUpload(props) {
const auth = useSelector(state => state.login.jwt);
const [speed, setSpeed] = useState(0); const [speed, setSpeed] = useState(0);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [result, setResult] = useState(); const [result, setResult] = useState();
@ -112,6 +114,9 @@ export function FileUpload(props) {
req.setRequestHeader("V-Content-Type", props.file.type); req.setRequestHeader("V-Content-Type", props.file.type);
req.setRequestHeader("V-Filename", props.file.name); req.setRequestHeader("V-Filename", props.file.name);
req.setRequestHeader("V-Digest", buf2hex(digest)); req.setRequestHeader("V-Digest", buf2hex(digest));
if (auth) {
req.setRequestHeader("Authorization", `Bearer ${auth}`);
}
if (typeof (editSecret) === "string") { if (typeof (editSecret) === "string") {
req.setRequestHeader("V-EditSecret", editSecret); req.setRequestHeader("V-EditSecret", editSecret);
} }

View File

@ -1,5 +1,5 @@
.footer { .footer {
margin-top: 10px; margin-top: 15px;
text-align: center; text-align: center;
} }

View File

@ -1,12 +1,21 @@
import "./FooterLinks.css" import "./FooterLinks.css"
import StrikeLogo from "./image/strike.png"; 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 ( return (
<div className="footer"> <div className="footer">
<a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a> <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://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> <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> </div>
); );
} }

View File

@ -3,9 +3,10 @@ import FeatherIcon from "feather-icons-react";
import {FormatBytes} from "./Util"; import {FormatBytes} from "./Util";
import "./GlobalStats.css"; import "./GlobalStats.css";
import {Api} from "./Api"; import {useApi} from "./Api";
export function GlobalStats(props) { export function GlobalStats(props) {
const {Api} = useApi();
let [stats, setStats] = useState(); let [stats, setStats] = useState();
async function loadStats() { async function loadStats() {

View File

@ -1,12 +0,0 @@
.home {
width: 720px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 720px) {
.home {
width: 100vw;
white-space: nowrap;
}
}

View File

@ -2,11 +2,9 @@
import {GlobalStats} from "./GlobalStats"; import {GlobalStats} from "./GlobalStats";
import {FooterLinks} from "./FooterLinks"; import {FooterLinks} from "./FooterLinks";
import "./HomePage.css";
export function HomePage() { export function HomePage() {
return ( return (
<div className="home"> <div className="page">
<Dropzone/> <Dropzone/>
<GlobalStats/> <GlobalStats/>
<FooterLinks/> <FooterLinks/>

View File

@ -3,9 +3,10 @@ import {useEffect} from "react";
import {Countdown} from "./Countdown"; import {Countdown} from "./Countdown";
import {PaywallOrderState} from "./Const"; import {PaywallOrderState} from "./Const";
import {Api} from "./Api"; import {useApi} from "./Api";
export function LightningPaywall(props) { export function LightningPaywall(props) {
const {Api} = useApi();
const file = props.file; const file = props.file;
const order = props.order; const order = props.order;
const onPaid = props.onPaid; const onPaid = props.onPaid;

View File

@ -1,11 +1,11 @@
import {useState} from "react"; import {useState} from "react";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import {setAuth} from "./LoginState"; import {setAuth} from "./LoginState";
import {useApi} from "./Api";
import "./Login.css"; import "./Login.css";
import {Api} from "./Api";
export function Login() { export function Login() {
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();
@ -19,12 +19,12 @@ export function Login() {
if (req.ok) { if (req.ok) {
let rsp = await req.json(); let rsp = await req.json();
if (rsp.jwt) { if (rsp.jwt) {
dispatch(setAuth(rsp.jwt)); dispatch(setAuth(rsp));
} else { } else {
setError(rsp.error); setError(rsp.error);
} }
} }
e.target.disabled = false; e.target.disabled = false;
} }

View File

@ -1,20 +1,25 @@
import {createSlice} from "@reduxjs/toolkit"; import {createSlice} from "@reduxjs/toolkit";
const LocalStorageKey = "token"; const LocalStorageKey = "token";
const LocalStorageProfileKey = "profile";
export const LoginState = createSlice({ export const LoginState = createSlice({
name: "Login", name: "Login",
initialState: { initialState: {
jwt: window.localStorage.getItem(LocalStorageKey) jwt: window.localStorage.getItem(LocalStorageKey),
profile: JSON.parse(window.localStorage.getItem(LocalStorageProfileKey))
}, },
reducers: { reducers: {
setAuth: (state, action) => { 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(LocalStorageKey, state.jwt);
window.localStorage.setItem(LocalStorageProfileKey, JSON.stringify(state.profile));
}, },
logout: (state) => { logout: (state) => {
state.jwt = null; state.jwt = null;
window.localStorage.removeItem(LocalStorageKey); window.localStorage.removeItem(LocalStorageKey);
window.localStorage.removeItem(LocalStorageProfileKey);
} }
} }
}); });

View File

@ -0,0 +1,5 @@
export function Profile() {
return (
<h1>Coming soon..</h1>
);
}

View 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>
)
}