Add user avatar URL
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +135,33 @@ 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;
|
||||||
|
await _userRepository.UpdateUser(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");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.AgentName = agentName;
|
// 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);
|
await _userRepository.UpdateUser(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import {closeGmxPositionImpl, getClientForAddress} from '../../src/plugins/custo
|
|||||||
import {TradeDirection} from '../../src/generated/ManagingApiTypes'
|
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,
|
||||||
'BNB',
|
'BNB',
|
||||||
TradeDirection.Long
|
TradeDirection.Long
|
||||||
)
|
)
|
||||||
console.log('Position closing result:', result)
|
console.log('Position closing result:', result)
|
||||||
assert.ok(result, 'Position closing result should be defined')
|
assert.ok(result, 'Position closing result should be defined')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 {
|
||||||
backtest: Backtest;
|
backtest: Backtest;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user