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

View File

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

View File

@ -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");

View File

@ -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();

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

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

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 './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>

View File

@ -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);

View File

@ -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}`;

View File

@ -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();

View File

@ -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);
}

View File

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

View File

@ -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>
);
}

View File

@ -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() {

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 {FooterLinks} from "./FooterLinks";
import "./HomePage.css";
export function HomePage() {
return (
<div className="home">
<div className="page">
<Dropzone/>
<GlobalStats/>
<FooterLinks/>

View File

@ -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;

View File

@ -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;
}

View File

@ -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);
}
}
});

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