diff --git a/VoidCat/spa/src/Admin/Admin.css b/VoidCat/spa/src/Admin/Admin.css
index ae1361a..776eaf6 100644
--- a/VoidCat/spa/src/Admin/Admin.css
+++ b/VoidCat/spa/src/Admin/Admin.css
@@ -9,23 +9,6 @@
padding: 10px;
}
-.admin table {
- width: 100%;
- word-break: keep-all;
- text-overflow: ellipsis;
- white-space: nowrap;
- border-collapse: collapse;
-}
-
-.admin table th {
- background-color: #222;
- text-align: start;
-}
-
-.admin table tr:nth-child(2n) {
- background-color: #111;
-}
-
.admin .btn {
padding: 5px 8px;
border-radius: 3px;
diff --git a/VoidCat/spa/src/Admin/Admin.js b/VoidCat/spa/src/Admin/Admin.js
index d448c95..a4ba857 100644
--- a/VoidCat/spa/src/Admin/Admin.js
+++ b/VoidCat/spa/src/Admin/Admin.js
@@ -5,17 +5,20 @@ import {UserList} from "./UserList";
import {Navigate} from "react-router-dom";
import {useApi} from "../Api";
import {VoidButton} from "../VoidButton";
+import {useState} from "react";
+import VoidModal from "../VoidModal";
+import EditUser from "./EditUser";
export function Admin() {
const auth = useSelector((state) => state.login.jwt);
const {AdminApi} = useApi();
-
+ const [editUser, setEditUser] = useState(null);
async function deleteFile(e, id) {
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
let req = await AdminApi.deleteFile(id);
if (req.ok) {
-
+
} else {
alert("Failed to delete file!");
}
@@ -28,7 +31,10 @@ export function Admin() {
return (
Users
-
+ [
+ Delete,
+ setEditUser(i)}>Edit
+ ]}/>
Files
{
@@ -36,6 +42,11 @@ export function Admin() {
deleteFile(e, i.id)}>Delete
}}/>
+
+ {editUser !== null ?
+
+ setEditUser(null)}/>
+ : null}
);
}
diff --git a/VoidCat/spa/src/Admin/EditUser.js b/VoidCat/spa/src/Admin/EditUser.js
new file mode 100644
index 0000000..f37c858
--- /dev/null
+++ b/VoidCat/spa/src/Admin/EditUser.js
@@ -0,0 +1,46 @@
+import {VoidButton} from "../VoidButton";
+import {useState} from "react";
+import {useSelector} from "react-redux";
+import {useApi} from "../Api";
+
+export default function EditUser(props) {
+ const user = props.user;
+ const onClose = props.onClose;
+
+ const adminApi = useApi().AdminApi;
+ const fileStores = useSelector((state) => state.info?.stats?.fileStores ?? ["local-disk"])
+ const [storage, setStorage] = useState(user.storage);
+ const [email, setEmail] = useState(user.email);
+
+ async function updateUser() {
+ await adminApi.updateUser({
+ id: user.id,
+ email,
+ storage
+ });
+ onClose();
+ }
+
+ return (
+ <>
+ Editing user '{user.displayName}' ({user.id})
+
+ - Email:
+ - setEmail(e.target.value)}/>
+
+ - File storage:
+ -
+
+
+
+ - Roles:
+ - {user.roles.map(e => {e})}
+
+ updateUser()}>Save
+ onClose()}>Cancel
+ >
+ );
+}
\ No newline at end of file
diff --git a/VoidCat/spa/src/Admin/UserList.js b/VoidCat/spa/src/Admin/UserList.js
index 099b5d5..d6d19e1 100644
--- a/VoidCat/spa/src/Admin/UserList.js
+++ b/VoidCat/spa/src/Admin/UserList.js
@@ -5,15 +5,15 @@ import {useApi} from "../Api";
import {logout} from "../LoginState";
import {PageSelector} from "../PageSelector";
import moment from "moment";
-import {VoidButton} from "../VoidButton";
-export function UserList() {
+export function UserList(props) {
const {AdminApi} = useApi();
const dispatch = useDispatch();
const [users, setUsers] = useState();
const [page, setPage] = useState(0);
const pageSize = 10;
const [accessDenied, setAccessDenied] = useState();
+ const actions = props.actions;
async function loadUserList() {
let pageReq = {
@@ -40,10 +40,7 @@ export function UserList() {
{moment(user.created).fromNow()} |
{moment(user.lastLogin).fromNow()} |
{obj.uploads} |
-
- Delete
- SetRoles
- |
+ {actions(user)} |
);
}
diff --git a/VoidCat/spa/src/Api.js b/VoidCat/spa/src/Api.js
index b33b33e..1b89953 100644
--- a/VoidCat/spa/src/Api.js
+++ b/VoidCat/spa/src/Api.js
@@ -22,12 +22,13 @@ export function useApi() {
body: body ? JSON.stringify(body) : undefined
});
}
-
+
return {
AdminApi: {
fileList: (pageReq) => getJson("POST", "/admin/file", pageReq, auth),
deleteFile: (id) => getJson("DELETE", `/admin/file/${id}`, undefined, auth),
- userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth)
+ userList: (pageReq) => getJson("POST", `/admin/users`, pageReq, auth),
+ updateUser: (user) => getJson("POST", `/admin/update-user`, user, auth)
},
Api: {
info: () => getJson("GET", "/info"),
@@ -42,7 +43,9 @@ export function useApi() {
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
submitVerifyCode: (uid, code) => getJson("POST", `/user/${uid}/verify`, code, auth),
sendNewCode: (uid) => getJson("GET", `/user/${uid}/verify`, undefined, auth),
- updateMetadata: (id, meta) => getJson("POST", `/upload/${id}/meta`, meta, auth)
+ updateMetadata: (id, meta) => getJson("POST", `/upload/${id}/meta`, meta, auth),
+ listApiKeys: () => getJson("GET", `/auth/api-key`, undefined, auth),
+ createApiKey: (req) => getJson("POST", `/auth/api-key`, req, auth)
}
};
}
\ No newline at end of file
diff --git a/VoidCat/spa/src/ApiKeyList.js b/VoidCat/spa/src/ApiKeyList.js
new file mode 100644
index 0000000..722d3ed
--- /dev/null
+++ b/VoidCat/spa/src/ApiKeyList.js
@@ -0,0 +1,71 @@
+import {useApi} from "./Api";
+import {useEffect, useState} from "react";
+import {VoidButton} from "./VoidButton";
+import moment from "moment";
+import VoidModal from "./VoidModal";
+
+export default function ApiKeyList() {
+ const {Api} = useApi();
+ const [apiKeys, setApiKeys] = useState([]);
+ const [newApiKey, setNewApiKey] = useState();
+ const DefaultExpiry = 1000 * 60 * 60 * 24 * 90;
+
+ async function loadApiKeys() {
+ let keys = await Api.listApiKeys();
+ setApiKeys(await keys.json());
+ }
+
+ async function createApiKey() {
+ let rsp = await Api.createApiKey({
+ expiry: new Date(new Date().getTime() + DefaultExpiry)
+ });
+ if (rsp.ok) {
+ setNewApiKey(await rsp.json());
+ }
+ }
+
+ useEffect(() => {
+ if (Api) {
+ loadApiKeys();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
API Keys
+
+
+ createApiKey()}>+New
+
+
+
+
+
+ Id |
+ Created |
+ Expiry |
+ Actions |
+
+
+
+ {apiKeys.map(e =>
+ {e.id} |
+ {moment(e.created).fromNow()} |
+ {moment(e.expiry).fromNow()} |
+
+ Delete
+ |
+
)}
+
+
+ {newApiKey ?
+
+ Please save this now as it will not be shown again:
+ {newApiKey.token}
+ setNewApiKey(undefined)}>Close
+ : null}
+ >
+ );
+}
\ No newline at end of file
diff --git a/VoidCat/spa/src/FileList.css b/VoidCat/spa/src/FileList.css
deleted file mode 100644
index 2b153b0..0000000
--- a/VoidCat/spa/src/FileList.css
+++ /dev/null
@@ -1,16 +0,0 @@
-table.file-list {
- width: 100%;
- word-break: keep-all;
- text-overflow: ellipsis;
- white-space: nowrap;
- border-collapse: collapse;
-}
-
-table.file-list tr:nth-child(2n) {
- background-color: #111;
-}
-
-table.file-list th {
- background-color: #222;
- text-align: start;
-}
\ No newline at end of file
diff --git a/VoidCat/spa/src/FileList.js b/VoidCat/spa/src/FileList.js
index 85cfe92..ac6be1c 100644
--- a/VoidCat/spa/src/FileList.js
+++ b/VoidCat/spa/src/FileList.js
@@ -1,4 +1,3 @@
-import "./FileList.css";
import moment from "moment";
import {Link} from "react-router-dom";
import {useDispatch} from "react-redux";
@@ -59,7 +58,7 @@ export function FileList(props) {
}
return (
-
+
Id |
diff --git a/VoidCat/spa/src/FileUpload.js b/VoidCat/spa/src/FileUpload.js
index 4dff93e..88004a3 100644
--- a/VoidCat/spa/src/FileUpload.js
+++ b/VoidCat/spa/src/FileUpload.js
@@ -87,11 +87,11 @@ export function FileUpload(props) {
* @param id {string}
* @param editSecret {string?}
* @param fullDigest {string?} Full file hash
+ * @param part {int?} Segment number
+ * @param partOf {int?} Total number of segments
* @returns {Promise}
*/
- async function xhrSegment(segment, id, editSecret, fullDigest) {
- setUState(UploadState.Hashing);
- const digest = await crypto.subtle.digest(DigestAlgo, segment);
+ async function xhrSegment(segment, id, editSecret, fullDigest, part, partOf) {
setUState(UploadState.Uploading);
return await new Promise((resolve, reject) => {
@@ -114,10 +114,10 @@ export function FileUpload(props) {
req.upload.onprogress = handleProgress;
req.open("POST", typeof (id) === "string" ? `${ApiHost}/upload/${id}` : `${ApiHost}/upload`);
req.setRequestHeader("Content-Type", "application/octet-stream");
- req.setRequestHeader("V-Content-Type", props.file.type);
+ req.setRequestHeader("V-Content-Type", props.file.type.length === 0 ? "application/octet-stream" : props.file.type);
req.setRequestHeader("V-Filename", props.file.name);
- req.setRequestHeader("V-Digest", buf2hex(digest));
req.setRequestHeader("V-Full-Digest", fullDigest);
+ req.setRequestHeader("V-Segment", `${part}/${partOf}`)
if (auth) {
req.setRequestHeader("Authorization", `Bearer ${auth}`);
}
@@ -136,14 +136,16 @@ export function FileUpload(props) {
// upload file in segments of 50MB
const UploadSize = 50_000_000;
+ setUState(UploadState.Hashing);
let digest = await crypto.subtle.digest(DigestAlgo, await props.file.arrayBuffer());
let xhr = null;
- const segments = props.file.size / UploadSize;
+ const segments = Math.ceil(props.file.size / UploadSize);
for (let s = 0; s < segments; s++) {
+ calc.ResetLastLoaded();
let offset = s * UploadSize;
let slice = props.file.slice(offset, offset + UploadSize, props.file.type);
let segment = await slice.arrayBuffer();
- xhr = await xhrSegment(segment, xhr?.file?.id, xhr?.file?.metadata?.editSecret, buf2hex(digest));
+ xhr = await xhrSegment(segment, xhr?.file?.id, xhr?.file?.metadata?.editSecret, buf2hex(digest), s + 1, segments);
if (!xhr.ok) {
break;
}
diff --git a/VoidCat/spa/src/Profile.js b/VoidCat/spa/src/Profile.js
index 04c47a0..d404aa9 100644
--- a/VoidCat/spa/src/Profile.js
+++ b/VoidCat/spa/src/Profile.js
@@ -10,6 +10,7 @@ import {buf2hex, hasFlag} from "./Util";
import moment from "moment";
import {FileList} from "./FileList";
import {VoidButton} from "./VoidButton";
+import ApiKeyList from "./ApiKeyList";
export function Profile() {
const [profile, setProfile] = useState();
@@ -210,6 +211,7 @@ export function Profile() {
{needsEmailVerify ? renderEmailVerify() : null}
Uploads
Api.listUserFiles(profile.id, req)}/>
+ {cantEditProfile ? : null}
);
diff --git a/VoidCat/spa/src/RateCalculator.js b/VoidCat/spa/src/RateCalculator.js
index 69effb0..b4a8495 100644
--- a/VoidCat/spa/src/RateCalculator.js
+++ b/VoidCat/spa/src/RateCalculator.js
@@ -4,6 +4,10 @@ export class RateCalculator {
this.lastLoaded = 0;
}
+ ResetLastLoaded() {
+ this.lastLoaded = 0;
+ }
+
ReportProgress(amount) {
this.reports.push({
time: new Date().getTime(),
diff --git a/VoidCat/spa/src/VoidModal.css b/VoidCat/spa/src/VoidModal.css
new file mode 100644
index 0000000..bb09e4f
--- /dev/null
+++ b/VoidCat/spa/src/VoidModal.css
@@ -0,0 +1,35 @@
+.modal-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-bg .modal {
+ min-height: 100px;
+ min-width: 300px;
+ background-color: #bbb;
+ color: #000;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.modal-bg .modal .modal-header {
+ text-align: center;
+ border-bottom: 1px solid;
+ margin: 0;
+ line-height: 2em;
+ background-color: #222;
+ color: #bbb;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.modal-bg .modal .modal-body {
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/VoidCat/spa/src/VoidModal.js b/VoidCat/spa/src/VoidModal.js
new file mode 100644
index 0000000..c3b90b9
--- /dev/null
+++ b/VoidCat/spa/src/VoidModal.js
@@ -0,0 +1,19 @@
+import "./VoidModal.css";
+
+export default function VoidModal(props) {
+ const title = props.title;
+ const style = props.style;
+
+ return (
+
+
+
+ {title ?? "Unknown modal"}
+
+
+ {props.children ?? "Missing body"}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/VoidCat/spa/src/index.css b/VoidCat/spa/src/index.css
index 22ef33b..462da48 100644
--- a/VoidCat/spa/src/index.css
+++ b/VoidCat/spa/src/index.css
@@ -67,4 +67,29 @@ input[type="text"], input[type="number"], input[type="password"], select {
padding: 10px 20px;
margin: 5px;
border: 0;
+}
+
+table {
+ width: 100%;
+ word-break: keep-all;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-collapse: collapse;
+}
+
+table tr:nth-child(2n) {
+ background-color: #111;
+}
+
+table th {
+ background-color: #222;
+ text-align: start;
+}
+
+pre.copy {
+ user-select: all;
+ width: fit-content;
+ border-radius: 4px;
+ border: 1px solid;
+ padding: 5px;
}
\ No newline at end of file