diff --git a/VoidCat/Controllers/AuthController.cs b/VoidCat/Controllers/AuthController.cs index 23face1..55305ed 100644 --- a/VoidCat/Controllers/AuthController.cs +++ b/VoidCat/Controllers/AuthController.cs @@ -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() { - 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); } diff --git a/VoidCat/Controllers/StatsController.cs b/VoidCat/Controllers/StatsController.cs index 4dd32f3..f52cfc1 100644 --- a/VoidCat/Controllers/StatsController.cs +++ b/VoidCat/Controllers/StatsController.cs @@ -19,6 +19,7 @@ namespace VoidCat.Controllers } [HttpGet] + [ResponseCache(Location = ResponseCacheLocation.Client, Duration = 60)] public async Task GetGlobalStats() { var bw = await _statsReporter.GetBandwidth(); diff --git a/VoidCat/Controllers/UploadController.cs b/VoidCat/Controllers/UploadController.cs index 6b22493..25a42e7 100644 --- a/VoidCat/Controllers/UploadController.cs +++ b/VoidCat/Controllers/UploadController.cs @@ -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"); diff --git a/VoidCat/Model/Extensions.cs b/VoidCat/Model/Extensions.cs index bcb2453..a0dc59e 100644 --- a/VoidCat/Model/Extensions.cs +++ b/VoidCat/Model/Extensions.cs @@ -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(); diff --git a/VoidCat/Model/VoidFileMeta.cs b/VoidCat/Model/VoidFileMeta.cs index 5f2703e..2abb479 100644 --- a/VoidCat/Model/VoidFileMeta.cs +++ b/VoidCat/Model/VoidFileMeta.cs @@ -52,6 +52,12 @@ public record VoidFileMeta : IVoidFileMeta /// SHA-256 hash of the file /// public string? Digest { get; init; } + + /// + /// User who uploaded the file + /// + [JsonConverter(typeof(Base58GuidConverter))] + public Guid? Uploader { get; init; } } /// diff --git a/VoidCat/Model/VoidUser.cs b/VoidCat/Model/VoidUser.cs index 179a67c..47cde86 100644 --- a/VoidCat/Model/VoidUser.cs +++ b/VoidCat/Model/VoidUser.cs @@ -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 diff --git a/VoidCat/Services/Files/LocalDiskFileStorage.cs b/VoidCat/Services/Files/LocalDiskFileStorage.cs index 206d263..1b4a36c 100644 --- a/VoidCat/Services/Files/LocalDiskFileStorage.cs +++ b/VoidCat/Services/Files/LocalDiskFileStorage.cs @@ -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 diff --git a/VoidCat/spa/src/Admin/FileList.js b/VoidCat/spa/src/Admin/FileList.js index 1b79af3..106dad6 100644 --- a/VoidCat/spa/src/Admin/FileList.js +++ b/VoidCat/spa/src/Admin/FileList.js @@ -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(); diff --git a/VoidCat/spa/src/Admin/UserList.js b/VoidCat/spa/src/Admin/UserList.js index a205ea5..ccf1e87 100644 --- a/VoidCat/spa/src/Admin/UserList.js +++ b/VoidCat/spa/src/Admin/UserList.js @@ -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(); diff --git a/VoidCat/spa/src/Api.js b/VoidCat/spa/src/Api.js index 0f39993..3de733e 100644 --- a/VoidCat/spa/src/Api.js +++ b/VoidCat/spa/src/Api.js @@ -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}) } \ No newline at end of file diff --git a/VoidCat/spa/src/App.css b/VoidCat/spa/src/App.css index 2ee17ac..b5c14f7 100644 --- a/VoidCat/spa/src/App.css +++ b/VoidCat/spa/src/App.css @@ -1,2 +1,12 @@ -.app { +.page { + width: 720px; + margin-left: auto; + margin-right: auto; +} + +@media (max-width: 720px) { + .page { + width: 100vw; + white-space: nowrap; + } } \ No newline at end of file diff --git a/VoidCat/spa/src/App.js b/VoidCat/spa/src/App.js index bbef9fe..54b752e 100644 --- a/VoidCat/spa/src/App.js +++ b/VoidCat/spa/src/App.js @@ -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() { }/> - }/> + }/> + }/> + }/> }/> diff --git a/VoidCat/spa/src/FileEdit.js b/VoidCat/spa/src/FileEdit.js index 69cdc9e..52ae953 100644 --- a/VoidCat/spa/src/FileEdit.js +++ b/VoidCat/spa/src/FileEdit.js @@ -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); diff --git a/VoidCat/spa/src/FilePaywall.js b/VoidCat/spa/src/FilePaywall.js index 95b3806..29f32fc 100644 --- a/VoidCat/spa/src/FilePaywall.js +++ b/VoidCat/spa/src/FilePaywall.js @@ -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}`; diff --git a/VoidCat/spa/src/FilePreview.js b/VoidCat/spa/src/FilePreview.js index ab4b5ad..678c893 100644 --- a/VoidCat/spa/src/FilePreview.js +++ b/VoidCat/spa/src/FilePreview.js @@ -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(); diff --git a/VoidCat/spa/src/FileUpload.js b/VoidCat/spa/src/FileUpload.js index 722caca..8b1e2be 100644 --- a/VoidCat/spa/src/FileUpload.js +++ b/VoidCat/spa/src/FileUpload.js @@ -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); } diff --git a/VoidCat/spa/src/FooterLinks.css b/VoidCat/spa/src/FooterLinks.css index 65eb2ca..5834bb6 100644 --- a/VoidCat/spa/src/FooterLinks.css +++ b/VoidCat/spa/src/FooterLinks.css @@ -1,5 +1,5 @@ .footer { - margin-top: 10px; + margin-top: 15px; text-align: center; } diff --git a/VoidCat/spa/src/FooterLinks.js b/VoidCat/spa/src/FooterLinks.js index 3336535..c3c332b 100644 --- a/VoidCat/spa/src/FooterLinks.js +++ b/VoidCat/spa/src/FooterLinks.js @@ -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 (
Discord Get Strike Strike logo GitHub + {!auth ? + Login : + Profile + }
); } \ No newline at end of file diff --git a/VoidCat/spa/src/GlobalStats.js b/VoidCat/spa/src/GlobalStats.js index 4feeeae..bcdad91 100644 --- a/VoidCat/spa/src/GlobalStats.js +++ b/VoidCat/spa/src/GlobalStats.js @@ -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() { diff --git a/VoidCat/spa/src/HomePage.css b/VoidCat/spa/src/HomePage.css deleted file mode 100644 index b52ba19..0000000 --- a/VoidCat/spa/src/HomePage.css +++ /dev/null @@ -1,12 +0,0 @@ -.home { - width: 720px; - margin-left: auto; - margin-right: auto; -} - -@media (max-width: 720px) { - .home { - width: 100vw; - white-space: nowrap; - } -} \ No newline at end of file diff --git a/VoidCat/spa/src/HomePage.js b/VoidCat/spa/src/HomePage.js index 4a47577..0023b39 100644 --- a/VoidCat/spa/src/HomePage.js +++ b/VoidCat/spa/src/HomePage.js @@ -2,11 +2,9 @@ import {GlobalStats} from "./GlobalStats"; import {FooterLinks} from "./FooterLinks"; -import "./HomePage.css"; - export function HomePage() { return ( -
+
diff --git a/VoidCat/spa/src/LightningPaywall.js b/VoidCat/spa/src/LightningPaywall.js index c8d14e5..ad3942a 100644 --- a/VoidCat/spa/src/LightningPaywall.js +++ b/VoidCat/spa/src/LightningPaywall.js @@ -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; diff --git a/VoidCat/spa/src/Login.js b/VoidCat/spa/src/Login.js index e1a2822..2dbc11e 100644 --- a/VoidCat/spa/src/Login.js +++ b/VoidCat/spa/src/Login.js @@ -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; } diff --git a/VoidCat/spa/src/LoginState.js b/VoidCat/spa/src/LoginState.js index 4f4ccd0..b91480c 100644 --- a/VoidCat/spa/src/LoginState.js +++ b/VoidCat/spa/src/LoginState.js @@ -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); } } }); diff --git a/VoidCat/spa/src/Profile.js b/VoidCat/spa/src/Profile.js new file mode 100644 index 0000000..4924772 --- /dev/null +++ b/VoidCat/spa/src/Profile.js @@ -0,0 +1,5 @@ +export function Profile() { + return ( +

Coming soon..

+ ); +} \ No newline at end of file diff --git a/VoidCat/spa/src/UserLogin.js b/VoidCat/spa/src/UserLogin.js new file mode 100644 index 0000000..e80d2c3 --- /dev/null +++ b/VoidCat/spa/src/UserLogin.js @@ -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 ( +
+ +
+ ) +} \ No newline at end of file