mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-08 12:38:00 +02:00
Edit profile
This commit is contained in:
parent
0a946d8f74
commit
709f8ef95f
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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}";
|
||||
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user