Add user avatar URL

This commit is contained in:
2025-05-13 11:31:03 +07:00
parent 4b0e87d48e
commit 621a5a745e
12 changed files with 196 additions and 31 deletions

View File

@@ -74,5 +74,18 @@ public class UserController : BaseController
var updatedUser = await _userService.UpdateAgentName(user, agentName); var updatedUser = await _userService.UpdateAgentName(user, agentName);
return Ok(updatedUser); return Ok(updatedUser);
} }
/// <summary>
/// Updates the avatar URL for the current user.
/// </summary>
/// <param name="avatarUrl">The new avatar URL to set.</param>
/// <returns>The updated user with the new avatar URL.</returns>
[HttpPut("avatar")]
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
{
var user = await GetUser();
var updatedUser = await _userService.UpdateAvatarUrl(user, avatarUrl);
return Ok(updatedUser);
}
} }

View File

@@ -7,5 +7,6 @@ public interface IUserService
Task<User> Authenticate(string name, string address, string message, string signature); Task<User> Authenticate(string name, string address, string message, string signature);
Task<User> GetUserByAddressAsync(string address); Task<User> GetUserByAddressAsync(string address);
Task<User> UpdateAgentName(User user, string agentName); Task<User> UpdateAgentName(User user, string agentName);
Task<User> UpdateAvatarUrl(User user, string avatarUrl);
User GetUser(string name); User GetUser(string name);
} }

View File

@@ -1,4 +1,5 @@
using Managing.Application.Abstractions.Repositories; using System.Text.RegularExpressions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
@@ -134,11 +135,34 @@ public class UserService : IUserService
var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName); var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName);
if (existingUser != null) if (existingUser != null)
{ {
throw new Exception("Agent name already used"); throw new Exception($"Agent name already used by {existingUser.Name}");
} }
else
{
user.AgentName = agentName; user.AgentName = agentName;
await _userRepository.UpdateUser(user); await _userRepository.UpdateUser(user);
return user; return user;
} }
} }
public async Task<User> UpdateAvatarUrl(User user, string avatarUrl)
{
// Validate URL format and image extension
if (!Uri.TryCreate(avatarUrl, UriKind.Absolute, out Uri? uriResult) ||
(uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
{
throw new Exception("Invalid URL format");
}
// Check for valid image extension
string pattern = @"\.(jpeg|jpg|png)$";
if (!Regex.IsMatch(avatarUrl, pattern, RegexOptions.IgnoreCase))
{
throw new Exception("URL must point to a JPEG or PNG image");
}
user.AvatarUrl = avatarUrl;
await _userRepository.UpdateUser(user);
return user;
}
}

View File

@@ -7,4 +7,5 @@ public class User
public string Name { get; set; } public string Name { get; set; }
public List<Account> Accounts { get; set; } public List<Account> Accounts { get; set; }
public string AgentName { get; set; } public string AgentName { get; set; }
public string AvatarUrl { get; set; }
} }

View File

@@ -8,4 +8,5 @@ public class UserDto : Document
{ {
public string Name { get; set; } public string Name { get; set; }
public string AgentName { get; set; } public string AgentName { get; set; }
public string AvatarUrl { get; set; }
} }

View File

@@ -532,7 +532,8 @@ public static class MongoMappers
return new User return new User
{ {
Name = user.Name, Name = user.Name,
AgentName = user.AgentName AgentName = user.AgentName,
AvatarUrl = user.AvatarUrl,
}; };
} }
@@ -541,7 +542,8 @@ public static class MongoMappers
return new UserDto return new UserDto
{ {
Name = user.Name, Name = user.Name,
AgentName = user.AgentName AgentName = user.AgentName,
AvatarUrl = user.AvatarUrl,
}; };
} }

View File

@@ -38,6 +38,7 @@ public class UserRepository : IUserRepository
{ {
var dto = await _userRepository.FindOneAsync(u => u.Name == user.Name); var dto = await _userRepository.FindOneAsync(u => u.Name == user.Name);
dto.AgentName = user.AgentName; dto.AgentName = user.AgentName;
dto.AvatarUrl = user.AvatarUrl;
_userRepository.Update(dto); _userRepository.Update(dto);
} }
catch (Exception e) catch (Exception e)

View File

@@ -459,8 +459,6 @@ export const closeGmxPositionImpl = async (
return position.marketInfo.indexToken.symbol === ticker && position.isLong === (direction === TradeDirection.Long); return position.marketInfo.indexToken.symbol === ticker && position.isLong === (direction === TradeDirection.Long);
}); });
console.log("positionsInfo", positionsInfo);
if (!positionKey) { if (!positionKey) {
throw new Error(`No open ${direction} position found for ${ticker}`); throw new Error(`No open ${direction} position found for ${ticker}`);
} }

View File

@@ -5,7 +5,7 @@ import {TradeDirection} from '../../src/generated/ManagingApiTypes'
test('GMX Position Closing', async (t) => { test('GMX Position Closing', async (t) => {
await t.test('should close a long position for BTC', async () => { await t.test('should close a long position for BTC', async () => {
const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78') const sdk = await getClientForAddress('0xbBA4eaA534cbD0EcAed5E2fD6036Aec2E7eE309f')
const result = await closeGmxPositionImpl( const result = await closeGmxPositionImpl(
sdk, sdk,

View File

@@ -1,5 +1,5 @@
import { TradeChart, CardPositionItem } from '..' import {CardPositionItem, TradeChart} from '..'
import { Backtest, MoneyManagement } from '../../../generated/ManagingApi' import {Backtest} from '../../../generated/ManagingApi'
import {CardPosition, CardText} from '../../mollecules' import {CardPosition, CardText} from '../../mollecules'
interface IBacktestRowDetailsProps { interface IBacktestRowDetailsProps {
@@ -21,7 +21,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
strategiesValues, strategiesValues,
signals, signals,
statistics, statistics,
moneyManagement config
} = backtest; } = backtest;
return ( return (
@@ -71,8 +71,8 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
<CardText <CardText
title="Money Management" title="Money Management"
content={ content={
"SL: " +(moneyManagement?.stopLoss * 100).toFixed(2) + "% TP: " + "SL: " +(config.moneyManagement?.stopLoss * 100).toFixed(2) + "% TP: " +
(moneyManagement?.takeProfit * 100).toFixed(2) + "%" (config.moneyManagement?.takeProfit * 100).toFixed(2) + "%"
} }
></CardText> ></CardText>
<CardText <CardText

View File

@@ -2323,6 +2323,45 @@ export class UserClient extends AuthorizedApiBase {
} }
return Promise.resolve<User>(null as any); return Promise.resolve<User>(null as any);
} }
user_UpdateAvatarUrl(avatarUrl: string): Promise<User> {
let url_ = this.baseUrl + "/User/avatar";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(avatarUrl);
let options_: RequestInit = {
body: content_,
method: "PUT",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processUser_UpdateAvatarUrl(_response);
});
}
protected processUser_UpdateAvatarUrl(response: Response): Promise<User> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<User>(null as any);
}
} }
export class WorkflowClient extends AuthorizedApiBase { export class WorkflowClient extends AuthorizedApiBase {
@@ -2519,6 +2558,7 @@ export interface User {
name?: string | null; name?: string | null;
accounts?: Account[] | null; accounts?: Account[] | null;
agentName?: string | null; agentName?: string | null;
avatarUrl?: string | null;
} }
export interface Balance { export interface Balance {

View File

@@ -10,8 +10,13 @@ type UpdateAgentNameForm = {
agentName: string agentName: string
} }
type UpdateAvatarForm = {
avatarUrl: string
}
function UserInfoSettings() { function UserInfoSettings() {
const [showUpdateModal, setShowUpdateModal] = useState(false) const [showUpdateModal, setShowUpdateModal] = useState(false)
const [showAvatarModal, setShowAvatarModal] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { apiUrl } = useApiUrlStore() const { apiUrl } = useApiUrlStore()
const api = new UserClient({}, apiUrl) const api = new UserClient({}, apiUrl)
@@ -22,12 +27,18 @@ function UserInfoSettings() {
}) })
const { const {
register, register: registerAgentName,
handleSubmit, handleSubmit: handleSubmitAgentName,
formState: { errors }, formState: { errors: agentNameErrors },
} = useForm<UpdateAgentNameForm>() } = useForm<UpdateAgentNameForm>()
const onSubmit = async (data: UpdateAgentNameForm) => { const {
register: registerAvatar,
handleSubmit: handleSubmitAvatar,
formState: { errors: avatarErrors },
} = useForm<UpdateAvatarForm>()
const onSubmitAgentName = async (data: UpdateAgentNameForm) => {
const toast = new Toast('Updating agent name') const toast = new Toast('Updating agent name')
try { try {
await api.user_UpdateAgentName(data.agentName) await api.user_UpdateAgentName(data.agentName)
@@ -40,6 +51,19 @@ function UserInfoSettings() {
} }
} }
const onSubmitAvatar = async (data: UpdateAvatarForm) => {
const toast = new Toast('Updating avatar')
try {
await api.user_UpdateAvatarUrl(data.avatarUrl)
queryClient.invalidateQueries({ queryKey: ['user'] })
setShowAvatarModal(false)
toast.update('success', 'Avatar updated successfully')
} catch (error) {
console.error('Error updating avatar:', error)
toast.update('error', 'Failed to update avatar')
}
}
return ( return (
<div className="container mx-auto p-4"> <div className="container mx-auto p-4">
<div className="bg-base-200 rounded-lg p-6 shadow-lg"> <div className="bg-base-200 rounded-lg p-6 shadow-lg">
@@ -61,13 +85,36 @@ function UserInfoSettings() {
Update Agent Name Update Agent Name
</button> </button>
</div> </div>
<div>
<label className="font-semibold">Avatar:</label>
<div className="mt-2 flex items-center space-x-4">
{user?.avatarUrl ? (
<img
src={user.avatarUrl}
alt="User avatar"
className="w-16 h-16 rounded-full object-cover"
/>
) : (
<div className="w-16 h-16 rounded-full bg-base-300 flex items-center justify-center">
<span className="text-2xl">{user?.name?.[0]?.toUpperCase() || '?'}</span>
</div>
)}
<button
className="btn btn-primary"
onClick={() => setShowAvatarModal(true)}
>
Update Avatar
</button>
</div>
</div>
</div> </div>
</div> </div>
<Modal <Modal
showModal={showUpdateModal} showModal={showUpdateModal}
onClose={() => setShowUpdateModal(false)} onClose={() => setShowUpdateModal(false)}
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmitAgentName(onSubmitAgentName)}
titleHeader="Update Agent Name" titleHeader="Update Agent Name"
> >
<div className="form-control w-full"> <div className="form-control w-full">
@@ -77,13 +124,50 @@ function UserInfoSettings() {
<input <input
type="text" type="text"
className="input input-bordered w-full" className="input input-bordered w-full"
{...register('agentName', { required: 'Agent name is required' })} {...registerAgentName('agentName', { required: 'Agent name is required' })}
defaultValue={user?.agentName || ''} defaultValue={user?.agentName || ''}
/> />
{errors.agentName && ( {agentNameErrors.agentName && (
<label className="label"> <label className="label">
<span className="label-text-alt text-error"> <span className="label-text-alt text-error">
{errors.agentName.message} {agentNameErrors.agentName.message}
</span>
</label>
)}
</div>
<div className="modal-action">
<button type="submit" className="btn">
Update
</button>
</div>
</Modal>
<Modal
showModal={showAvatarModal}
onClose={() => setShowAvatarModal(false)}
onSubmit={handleSubmitAvatar(onSubmitAvatar)}
titleHeader="Update Avatar"
>
<div className="form-control w-full">
<label className="label">
<span className="label-text">Avatar URL (JPEG or PNG)</span>
</label>
<input
type="url"
className="input input-bordered w-full"
{...registerAvatar('avatarUrl', {
required: 'Avatar URL is required',
pattern: {
value: /^https?:\/\/.+\.(jpeg|jpg|png)$/i,
message: 'URL must be a valid image URL ending in .jpeg, .jpg, or .png'
}
})}
defaultValue={user?.avatarUrl || ''}
/>
{avatarErrors.avatarUrl && (
<label className="label">
<span className="label-text-alt text-error">
{avatarErrors.avatarUrl.message}
</span> </span>
</label> </label>
)} )}