Edit profile

This commit is contained in:
Kieran 2022-02-27 18:15:37 +00:00
parent 0a946d8f74
commit 709f8ef95f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 183 additions and 16 deletions

View File

@ -24,9 +24,28 @@ public class UserController : Controller
{
return await _store.Get<PrivateVoidUser>(id.FromBase58Guid());
}
else
return await _store.Get<PublicVoidUser>(id.FromBase58Guid());
}
[HttpPost]
[Route("{id}")]
public async Task<IActionResult> UpdateUser([FromRoute] string id, [FromBody] PublicVoidUser user)
{
var loggedUser = HttpContext.GetUserId();
var requestedId = id.FromBase58Guid();
if (requestedId != loggedUser)
{
return await _store.Get<PublicVoidUser>(id.FromBase58Guid());
return Unauthorized();
}
// check requested user is same as user obj
if (requestedId != user.Id)
{
return BadRequest();
}
await _store.Update(user);
return Ok();
}
}

View File

@ -8,4 +8,5 @@ public interface IUserStore
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
ValueTask Set(InternalVoidUser user);
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
ValueTask Update(PublicVoidUser newUser);
}

View File

@ -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<InternalVoidUser>(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}";

View File

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

View File

@ -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<any>}
*/
async function xhrSegment(segment, id, editSecret) {
const DigestAlgo = "SHA-256";
setUState(UploadState.Hashing);
const digest = await crypto.subtle.digest(DigestAlgo, segment);
setUState(UploadState.Uploading);

View File

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

View File

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

View File

@ -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 (
<div className="page">
<div className="profile">
<h2>{profile.displayName}</h2>
<div className="avatar" style={avatarStyles}/>
<div className="name">
{canEdit ?
<input value={profile.displayName}
onChange={(e) => editUsername(e.target.value)}/>
: profile.displayName}
</div>
<div className="avatar" style={avatarStyles}>
{canEdit ? <div className="edit-avatar" onClick={() => changeAvatar()}>
<h3>Edit</h3>
</div> : null}
</div>
<div className="roles">
<h3>Roles:</h3>
{profile.roles.map(a => <span className="btn">{a}</span>)}
</div>
{canEdit ?
<Fragment>
<p>
<label>Public Profile:</label>
<input type="checkbox" checked={profile.public}
onChange={(e) => editPublic(e.target.checked)}/>
</p>
<div className="btn" onClick={saveUser}>Save</div>
</Fragment> : null}
</div>
</div>
);

View File

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