diff --git a/VoidCat/Controllers/UserController.cs b/VoidCat/Controllers/UserController.cs index e446797..80d7566 100644 --- a/VoidCat/Controllers/UserController.cs +++ b/VoidCat/Controllers/UserController.cs @@ -24,9 +24,28 @@ public class UserController : Controller { return await _store.Get(id.FromBase58Guid()); } - else + + return await _store.Get(id.FromBase58Guid()); + } + + [HttpPost] + [Route("{id}")] + public async Task UpdateUser([FromRoute] string id, [FromBody] PublicVoidUser user) + { + var loggedUser = HttpContext.GetUserId(); + var requestedId = id.FromBase58Guid(); + if (requestedId != loggedUser) { - return await _store.Get(id.FromBase58Guid()); + return Unauthorized(); } + + // check requested user is same as user obj + if (requestedId != user.Id) + { + return BadRequest(); + } + + await _store.Update(user); + return Ok(); } } diff --git a/VoidCat/Services/Abstractions/IUserStore.cs b/VoidCat/Services/Abstractions/IUserStore.cs index c112ac4..8c9dd28 100644 --- a/VoidCat/Services/Abstractions/IUserStore.cs +++ b/VoidCat/Services/Abstractions/IUserStore.cs @@ -8,4 +8,5 @@ public interface IUserStore ValueTask Get(Guid id) where T : VoidUser; ValueTask Set(InternalVoidUser user); ValueTask> ListUsers(PagedRequest request); + ValueTask Update(PublicVoidUser newUser); } \ No newline at end of file diff --git a/VoidCat/Services/Users/UserStore.cs b/VoidCat/Services/Users/UserStore.cs index c916626..6c2f2fe 100644 --- a/VoidCat/Services/Users/UserStore.cs +++ b/VoidCat/Services/Users/UserStore.cs @@ -60,6 +60,19 @@ public class UserStore : IUserStore Results = EnumerateUsers(users?.Skip(request.PageSize * request.Page).Take(request.PageSize)) }; } + + public async ValueTask Update(PublicVoidUser newUser) + { + var oldUser = await Get(newUser.Id); + if (oldUser == null) return; + + // update only a few props + oldUser.Avatar = newUser.Avatar; + oldUser.Public = newUser.Public; + oldUser.DisplayName = newUser.DisplayName; + + await Set(oldUser); + } private static string MapKey(Guid id) => $"user:{id}"; private static string MapKey(string email) => $"user:email:{email}"; diff --git a/VoidCat/spa/src/Api.js b/VoidCat/spa/src/Api.js index 5665cff..87da903 100644 --- a/VoidCat/spa/src/Api.js +++ b/VoidCat/spa/src/Api.js @@ -37,7 +37,8 @@ export function useApi() { 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}), - getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth) + getUser: (id) => getJson("GET", `/user/${id}`, undefined, auth), + updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth) } }; } \ No newline at end of file diff --git a/VoidCat/spa/src/FileUpload.js b/VoidCat/spa/src/FileUpload.js index 071f016..71c475f 100644 --- a/VoidCat/spa/src/FileUpload.js +++ b/VoidCat/spa/src/FileUpload.js @@ -16,6 +16,8 @@ const UploadState = { Challenge: 6 }; +export const DigestAlgo = "SHA-256"; + export function FileUpload(props) { const auth = useSelector(state => state.login.jwt); const [speed, setSpeed] = useState(0); @@ -87,7 +89,6 @@ export function FileUpload(props) { * @returns {Promise} */ async function xhrSegment(segment, id, editSecret) { - const DigestAlgo = "SHA-256"; setUState(UploadState.Hashing); const digest = await crypto.subtle.digest(DigestAlgo, segment); setUState(UploadState.Uploading); diff --git a/VoidCat/spa/src/LoginState.js b/VoidCat/spa/src/LoginState.js index b91480c..ffbaac1 100644 --- a/VoidCat/spa/src/LoginState.js +++ b/VoidCat/spa/src/LoginState.js @@ -16,6 +16,10 @@ export const LoginState = createSlice({ window.localStorage.setItem(LocalStorageKey, state.jwt); window.localStorage.setItem(LocalStorageProfileKey, JSON.stringify(state.profile)); }, + setProfile: (state, action) => { + state.profile = action.payload; + window.localStorage.setItem(LocalStorageProfileKey, JSON.stringify(state.profile)); + }, logout: (state) => { state.jwt = null; window.localStorage.removeItem(LocalStorageKey); @@ -24,5 +28,5 @@ export const LoginState = createSlice({ } }); -export const {setAuth, logout} = LoginState.actions; +export const {setAuth, setProfile, logout} = LoginState.actions; export default LoginState.reducer; \ No newline at end of file diff --git a/VoidCat/spa/src/Profile.css b/VoidCat/spa/src/Profile.css index 2692841..03f9d8c 100644 --- a/VoidCat/spa/src/Profile.css +++ b/VoidCat/spa/src/Profile.css @@ -1,5 +1,18 @@ .profile { - + +} + +.profile .name { + font-size: 30px; + margin: 10px 0; +} + +.profile .name input { + background: unset; + color: white; + font-size: inherit; + line-height: inherit; + border: unset; } .profile .avatar { @@ -9,4 +22,23 @@ background-position: center; background-repeat: no-repeat; background-size: cover; +} + +.profile .avatar .edit-avatar { + opacity: 0; + background: rgba(0, 0, 0, 0.4); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.profile .avatar .edit-avatar:hover { + opacity: 1; +} + +.profile .roles { + margin: 20px 0; } \ No newline at end of file diff --git a/VoidCat/spa/src/Profile.js b/VoidCat/spa/src/Profile.js index 5b90eb0..554880f 100644 --- a/VoidCat/spa/src/Profile.js +++ b/VoidCat/spa/src/Profile.js @@ -1,14 +1,21 @@ -import {useEffect, useState} from "react"; +import {Fragment, useEffect, useState} from "react"; import {useParams} from "react-router-dom"; import {useApi} from "./Api"; -import {DefaultAvatar} from "./Const"; - +import {ApiHost, DefaultAvatar} from "./Const"; import "./Profile.css"; +import {useDispatch, useSelector} from "react-redux"; +import {setProfile as setGlobalProfile} from "./LoginState"; +import {DigestAlgo} from "./FileUpload"; +import {buf2hex} from "./Util"; export function Profile() { const [profile, setProfile] = useState(); + const auth = useSelector(state => state.login.jwt); + const localProfile = useSelector(state => state.login.profile); + const canEdit = localProfile?.id === profile?.id; const {Api} = useApi(); const params = useParams(); + const dispatch = useDispatch(); async function loadProfile() { let p = await Api.getUser(params.id); @@ -17,23 +24,113 @@ export function Profile() { } } + function editUsername(v) { + setProfile({ + ...profile, + displayName: v + }); + } + + function editPublic(v) { + setProfile({ + ...profile, + public: v + }); + } + + async function changeAvatar() { + let res = await new Promise((resolve, reject) => { + let i = document.createElement('input'); + i.setAttribute('type', 'file'); + i.setAttribute('multiple', ''); + i.addEventListener('change', async function (evt) { + resolve(evt.target.files); + }); + i.click(); + }); + + const file = res[0]; + const buf = await file.arrayBuffer(); + const digest = await crypto.subtle.digest(DigestAlgo, buf); + + let req = await fetch(`${ApiHost}/upload`, { + mode: "cors", + method: "POST", + body: buf, + headers: { + "Content-Type": "application/octet-stream", + "V-Content-Type": file.type, + "V-Filename": file.name, + "V-Digest": buf2hex(digest), + "Authorization": `Bearer ${auth}` + } + }); + + if(req.ok) { + let rsp = await req.json(); + if(rsp.ok) { + setProfile({ + ...profile, + avatar: rsp.file.id + }); + } + } + + } + + async function saveUser() { + let r = await Api.updateUser({ + id: profile.id, + avatar: profile.avatar, + displayName: profile.displayName, + public: profile.public + }); + if (r.ok) { + // saved + dispatch(setGlobalProfile(profile)); + } + } + useEffect(() => { loadProfile(); }, []); if (profile) { + let avatarUrl = profile.avatar ?? DefaultAvatar; + if(!avatarUrl.startsWith("http")) { + // assume void-cat hosted avatar + avatarUrl = `/d/${avatarUrl}`; + } let avatarStyles = { - backgroundImage: `url(${profile.avatar ?? DefaultAvatar})` + backgroundImage: `url(${avatarUrl})` }; return (
-

{profile.displayName}

-
+
+ {canEdit ? + editUsername(e.target.value)}/> + : profile.displayName} +
+
+ {canEdit ?
changeAvatar()}> +

Edit

+
: null} +

Roles:

{profile.roles.map(a => {a})}
+ {canEdit ? + +

+ + editPublic(e.target.checked)}/> +

+
Save
+
: null}
); diff --git a/VoidCat/spa/src/index.css b/VoidCat/spa/src/index.css index 93929ca..7423c6a 100644 --- a/VoidCat/spa/src/index.css +++ b/VoidCat/spa/src/index.css @@ -20,14 +20,13 @@ a:hover { .btn { display: inline-block; - line-height: 1.3; - font-size: large; + line-height: 1.1; font-weight: bold; text-transform: uppercase; - border-radius: 20px; + border-radius: 10px; background-color: white; color: black; - padding: 10px 30px; + padding: 10px 20px; user-select: none; cursor: pointer; } \ No newline at end of file