forked from Kieran/void.cat
Allow editing metadata
This commit is contained in:
parent
20d32ad6d3
commit
285899b742
@ -194,8 +194,7 @@ namespace VoidCat.Controllers
|
|||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||||
if (meta == default) return NotFound();
|
if (meta == default) return NotFound();
|
||||||
|
if (!meta.CanEdit(req.EditSecret, HttpContext)) return Unauthorized();
|
||||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
|
||||||
|
|
||||||
if (req.Strike != default)
|
if (req.Strike != default)
|
||||||
{
|
{
|
||||||
@ -207,6 +206,28 @@ namespace VoidCat.Controllers
|
|||||||
await _paywall.Set(gid, new NoPaywallConfig());
|
await _paywall.Set(gid, new NoPaywallConfig());
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update metadata about file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Id of file to edit</param>
|
||||||
|
/// <param name="fileMeta">New metadata to update</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// You can only change `Name`, `Description` and `MimeType`
|
||||||
|
/// </remarks>
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{id}/meta")]
|
||||||
|
public async Task<IActionResult> UpdateFileMeta([FromRoute] string id, [FromBody] SecretVoidFileMeta fileMeta)
|
||||||
|
{
|
||||||
|
var gid = id.FromBase58Guid();
|
||||||
|
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||||
|
if (meta == default) return NotFound();
|
||||||
|
if (!meta.CanEdit(fileMeta.EditSecret, HttpContext)) return Unauthorized();
|
||||||
|
|
||||||
|
await _metadata.Update(gid, fileMeta);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
@ -61,6 +61,12 @@ public static class Extensions
|
|||||||
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool CanEdit(this SecretVoidFileMeta file, Guid? editSecret, HttpContext context)
|
||||||
|
{
|
||||||
|
return file.EditSecret == editSecret
|
||||||
|
|| file.Uploader == context.GetUserId();
|
||||||
|
}
|
||||||
|
|
||||||
public static string ToHex(this byte[] data)
|
public static string ToHex(this byte[] data)
|
||||||
{
|
{
|
||||||
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
|
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
|
||||||
|
@ -26,7 +26,7 @@ public record VoidFileMeta : IVoidFileMeta
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filename
|
/// Filename
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Name { get; init; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Size of the file in storage
|
/// Size of the file in storage
|
||||||
@ -41,12 +41,12 @@ public record VoidFileMeta : IVoidFileMeta
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Description about the file
|
/// Description about the file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Description { get; init; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The content type of the file
|
/// The content type of the file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? MimeType { get; init; }
|
public string? MimeType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA-256 hash of the file
|
/// SHA-256 hash of the file
|
||||||
|
@ -5,4 +5,5 @@ namespace VoidCat.Services.Abstractions;
|
|||||||
public interface IFileMetadataStore : IPublicPrivateStore<VoidFileMeta, SecretVoidFileMeta>
|
public interface IFileMetadataStore : IPublicPrivateStore<VoidFileMeta, SecretVoidFileMeta>
|
||||||
{
|
{
|
||||||
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
|
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
|
||||||
|
ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Model.Exceptions;
|
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Files;
|
namespace VoidCat.Services.Files;
|
||||||
@ -28,6 +27,18 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
|||||||
return GetMeta<TMeta>(id);
|
return GetMeta<TMeta>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta
|
||||||
|
{
|
||||||
|
var oldMeta = await GetMeta<SecretVoidFileMeta>(id);
|
||||||
|
if (oldMeta == default) return;
|
||||||
|
|
||||||
|
oldMeta.Description = meta.Description ?? oldMeta.Description;
|
||||||
|
oldMeta.Name = meta.Name ?? oldMeta.Name;
|
||||||
|
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
|
||||||
|
|
||||||
|
await Set(id, oldMeta);
|
||||||
|
}
|
||||||
|
|
||||||
public ValueTask<VoidFileMeta?> Get(Guid id)
|
public ValueTask<VoidFileMeta?> Get(Guid id)
|
||||||
{
|
{
|
||||||
return GetMeta<VoidFileMeta>(id);
|
return GetMeta<VoidFileMeta>(id);
|
||||||
|
@ -25,6 +25,18 @@ public class S3FileMetadataStore : IFileMetadataStore
|
|||||||
return GetMeta<TMeta>(id);
|
return GetMeta<TMeta>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta
|
||||||
|
{
|
||||||
|
var oldMeta = await GetMeta<SecretVoidFileMeta>(id);
|
||||||
|
if (oldMeta == default) return;
|
||||||
|
|
||||||
|
oldMeta.Description = meta.Description ?? oldMeta.Description;
|
||||||
|
oldMeta.Name = meta.Name ?? oldMeta.Name;
|
||||||
|
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
|
||||||
|
|
||||||
|
await Set(id, oldMeta);
|
||||||
|
}
|
||||||
|
|
||||||
public ValueTask<VoidFileMeta?> Get(Guid id)
|
public ValueTask<VoidFileMeta?> Get(Guid id)
|
||||||
{
|
{
|
||||||
return GetMeta<VoidFileMeta>(id);
|
return GetMeta<VoidFileMeta>(id);
|
||||||
|
@ -41,7 +41,8 @@ export function useApi() {
|
|||||||
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
||||||
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
||||||
submitVerifyCode: (uid, code) => getJson("POST", `/user/${uid}/verify`, code, auth),
|
submitVerifyCode: (uid, code) => getJson("POST", `/user/${uid}/verify`, code, auth),
|
||||||
sendNewCode: (uid) => getJson("GET", `/user/${uid}/verify`, undefined, auth)
|
sendNewCode: (uid) => getJson("GET", `/user/${uid}/verify`, undefined, auth),
|
||||||
|
updateMetadata: (id, meta) => getJson("POST", `/upload/${id}/meta`, meta, auth)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import {NoPaywallConfig} from "./NoPaywallConfig";
|
|||||||
import {useApi} from "./Api";
|
import {useApi} from "./Api";
|
||||||
import "./FileEdit.css";
|
import "./FileEdit.css";
|
||||||
import {useSelector} from "react-redux";
|
import {useSelector} from "react-redux";
|
||||||
|
import {VoidButton} from "./VoidButton";
|
||||||
|
|
||||||
export function FileEdit(props) {
|
export function FileEdit(props) {
|
||||||
const {Api} = useApi();
|
const {Api} = useApi();
|
||||||
@ -25,6 +26,15 @@ export function FileEdit(props) {
|
|||||||
return req.ok;
|
return req.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMeta() {
|
||||||
|
let meta = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
editSecret: privateFile?.metadata?.editSecret
|
||||||
|
};
|
||||||
|
await Api.updateMetadata(file.id, meta);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPaywallConfig() {
|
function renderPaywallConfig() {
|
||||||
switch (paywall) {
|
switch (paywall) {
|
||||||
case 0: {
|
case 0: {
|
||||||
@ -47,7 +57,7 @@ export function FileEdit(props) {
|
|||||||
<dt>Description:</dt>
|
<dt>Description:</dt>
|
||||||
<dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd>
|
<dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
<VoidButton onClick={(e) => saveMeta()} options={{showSuccess: true}}>Save</VoidButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flx-1">
|
<div className="flx-1">
|
||||||
<h3>Paywall Config</h3>
|
<h3>Paywall Config</h3>
|
||||||
|
@ -8,14 +8,12 @@ import {logout, setProfile as setGlobalProfile} from "./LoginState";
|
|||||||
import {DigestAlgo} from "./FileUpload";
|
import {DigestAlgo} from "./FileUpload";
|
||||||
import {buf2hex, hasFlag} from "./Util";
|
import {buf2hex, hasFlag} from "./Util";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import FeatherIcon from "feather-icons-react";
|
|
||||||
import {FileList} from "./FileList";
|
import {FileList} from "./FileList";
|
||||||
import {VoidButton} from "./VoidButton";
|
import {VoidButton} from "./VoidButton";
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const [profile, setProfile] = useState();
|
const [profile, setProfile] = useState();
|
||||||
const [noProfile, setNoProfile] = useState(false);
|
const [noProfile, setNoProfile] = useState(false);
|
||||||
const [saved, setSaved] = useState(false);
|
|
||||||
const [emailCode, setEmailCode] = useState("");
|
const [emailCode, setEmailCode] = useState("");
|
||||||
const [emailCodeError, setEmailCodeError] = useState("");
|
const [emailCodeError, setEmailCodeError] = useState("");
|
||||||
const [newCodeSent, setNewCodeSent] = useState(false);
|
const [newCodeSent, setNewCodeSent] = useState(false);
|
||||||
@ -103,7 +101,6 @@ export function Profile() {
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
// saved
|
// saved
|
||||||
dispatch(setGlobalProfile(profile));
|
dispatch(setGlobalProfile(profile));
|
||||||
setSaved(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +157,7 @@ export function Profile() {
|
|||||||
</dl>
|
</dl>
|
||||||
<div className="flex flex-center">
|
<div className="flex flex-center">
|
||||||
<div>
|
<div>
|
||||||
<VoidButton onClick={saveUser}>Save</VoidButton>
|
<VoidButton onClick={saveUser} options={{showSuccess: true}}>Save</VoidButton>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{saved ? <FeatherIcon icon="check-circle"/> : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<VoidButton onClick={() => dispatch(logout())}>Logout</VoidButton>
|
<VoidButton onClick={() => dispatch(logout())}>Logout</VoidButton>
|
||||||
@ -177,12 +171,6 @@ export function Profile() {
|
|||||||
loadProfile();
|
loadProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (saved === true) {
|
|
||||||
setTimeout(() => setSaved(false), 1000);
|
|
||||||
}
|
|
||||||
}, [saved]);
|
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||||
if (!avatarUrl.startsWith("http")) {
|
if (!avatarUrl.startsWith("http")) {
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import FeatherIcon from "feather-icons-react";
|
||||||
|
|
||||||
export function VoidButton(props) {
|
export function VoidButton(props) {
|
||||||
|
const options = {
|
||||||
|
showSuccess: false,
|
||||||
|
...props.options
|
||||||
|
};
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
async function handleClick(e) {
|
async function handleClick(e) {
|
||||||
if (e.target.classList.contains("disabled")) return;
|
if (e.target.classList.contains("disabled")) return;
|
||||||
e.target.classList.add("disabled");
|
e.target.classList.add("disabled");
|
||||||
@ -9,10 +18,24 @@ export function VoidButton(props) {
|
|||||||
if (typeof ret === "object" && typeof ret.then === "function") {
|
if (typeof ret === "object" && typeof ret.then === "function") {
|
||||||
await ret;
|
await ret;
|
||||||
}
|
}
|
||||||
|
setSuccess(options.showSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
e.target.classList.remove("disabled");
|
e.target.classList.remove("disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="btn" onClick={handleClick}>{props.children}</div>;
|
useEffect(() => {
|
||||||
|
if (success === true) {
|
||||||
|
setTimeout(() => setSuccess(false), 1000);
|
||||||
|
}
|
||||||
|
}, [success]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-inline flex-center">
|
||||||
|
<div>
|
||||||
|
<div className="btn" onClick={handleClick}>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
{success ? <div><FeatherIcon icon="check-circle"/></div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -40,6 +40,10 @@ a:hover {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.flx-1 {
|
.flx-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user