diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/UserAvatarUpdateInput.cs b/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/UserAvatarUpdateInput.cs new file mode 100644 index 0000000..7879827 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/Identity/Dto/UserAvatarUpdateInput.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.Content; + +namespace Sozsoft.Platform.Identity.Dto; + +public class UserAvatarUpdateInput +{ + public Guid UserId { get; set; } + + public IRemoteStreamContent Avatar { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs index 80dd656..c644ab8 100644 --- a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Sozsoft.Platform.BlobStoring; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Identity.Dto; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; @@ -32,6 +34,7 @@ public class PlatformIdentityAppService : ApplicationService public IRepository workHourRepository { get; } public IRepository departmentRepository { get; } public IRepository jobPositionRepository { get; } + public BlobManager BlobCdnManager { get; } public PlatformIdentityAppService( IIdentityUserAppService identityUserAppService, @@ -45,6 +48,7 @@ public class PlatformIdentityAppService : ApplicationService IRepository workHourRepository, IRepository departmentRepository, IRepository jobPositionRepository, + BlobManager blobCdnManager, IGuidGenerator guidGenerator ) { @@ -55,6 +59,7 @@ public class PlatformIdentityAppService : ApplicationService this.workHourRepository = workHourRepository; this.departmentRepository = departmentRepository; this.jobPositionRepository = jobPositionRepository; + this.BlobCdnManager = blobCdnManager; this.permissionRepository = permissionRepository; this.branchRepository = branchRepository; this.branchUsersRepository = branchUsersRepository; @@ -273,6 +278,22 @@ public class PlatformIdentityAppService : ApplicationService await UserManager.UpdateAsync(user); } + [Authorize(IdentityPermissions.Users.Update)] + public async Task UpdateAvatarAsync([FromForm] UserAvatarUpdateInput input) + { + var user = await UserManager.GetByIdAsync(input.UserId); + var fileName = $"{user.Id}.jpg"; + + if (input.Avatar is null || input.Avatar.ContentLength == 0) + { + await BlobCdnManager.DeleteAsync(BlobContainerNames.Avatar, fileName); + } + else + { + await BlobCdnManager.SaveAsync(BlobContainerNames.Avatar, fileName, input.Avatar.GetStream()); + } + } + public async Task> GetPermissionList() { var list = await permissionRepository.GetListAsync(); diff --git a/ui/src/services/identity.service.ts b/ui/src/services/identity.service.ts index 042e27c..19d2dba 100644 --- a/ui/src/services/identity.service.ts +++ b/ui/src/services/identity.service.ts @@ -11,6 +11,11 @@ import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '. import { AuditLogDto } from '../proxy/auditLog/audit-log' import apiService from './api.service' +export interface UserAvatarUpdateInput { + userId: string + avatar?: File +} + export const getRoles = (skipCount = 0, maxResultCount = 10) => apiService.fetchData>({ method: 'GET', @@ -36,6 +41,21 @@ export const putUserDetail = (input: UserInfoViewModel) => data: input, }) +export const putUserAvatar = (input: UserAvatarUpdateInput) => { + const formData = new FormData() + formData.append('userId', input.userId) + + if (input.avatar) { + formData.append('avatar', input.avatar) + } + + return apiService.fetchData({ + method: 'PUT', + url: `/api/app/platform-identity/avatar`, + data: formData, + }) +} + export const putUserLookout = (input: UserInfoViewModel) => apiService.fetchData({ method: 'PUT', diff --git a/ui/src/views/admin/user-management/Details.tsx b/ui/src/views/admin/user-management/Details.tsx index a8bc8ba..4e855d9 100644 --- a/ui/src/views/admin/user-management/Details.tsx +++ b/ui/src/views/admin/user-management/Details.tsx @@ -26,11 +26,11 @@ import { deleteClaimUser, getUserDetail, postClaimUser, + putUserAvatar, putUserDetail, putUserLookout, putUserPermission, } from '@/services/identity.service' -import { updateProfile } from '@/services/account.service' import { CountryDto, getCountry } from '@/services/home.service' import { useLocalization } from '@/utils/hooks/useLocalization' import dayjs from 'dayjs' @@ -97,6 +97,7 @@ function UserDetails() { const [confirmDeleteClaim, setConfirmDeleteClaim] = useState(null) const [countries, setCountries] = useState([]) const [image, setImage] = useState() + const [hasAvatarChange, setHasAvatarChange] = useState(false) const cropperRef = useRef(null) const previewRef = useRef(null) const auth = useStoreState((state) => state.auth) @@ -111,15 +112,26 @@ function UserDetails() { const isTwoFactorEnabled = setting('Abp.Account.TwoFactor.Enabled') const getUser = async (syncAvatar = true) => { - const { data } = await getUserDetail(userId || '') + if (!userId) { + return + } + + const { data } = await getUserDetail(userId) setUserDetails(data) if (syncAvatar) { setImage(`${AVATAR_URL(data.id, data.tenantId)}?${dayjs().unix()}`) + setHasAvatarChange(false) } } useEffect(() => { + setUserDetails(undefined) + setImage(undefined) + setHasAvatarChange(false) getUser() + }, [userId]) + + useEffect(() => { getCountry().then(({ data }) => setCountries(data)) }, []) @@ -131,8 +143,10 @@ function UserDetails() { const onChooseImage = async (file: File[]) => { if (file[0]) { setImage(URL.createObjectURL(file[0])) + setHasAvatarChange(true) } else { setImage(undefined) + setHasAvatarChange(true) } } @@ -246,34 +260,49 @@ function UserDetails() {
{ setSubmitting(true) await putUserDetail({ ...values }) - let keepCurrentAvatar = false - - const avatar = await getCroppedAvatar() - const resp = await updateProfile({ - name: values.name ?? '', - surname: values.surname ?? '', - avatar: avatar ? new File([avatar], 'avatar') : undefined, - }) - - if (resp.status === 200) { - const avatarUrl = - AVATAR_URL(auth.user.id, auth.tenant?.tenantId) + `?${dayjs().unix()}` - + if (values.id === auth.user.id) { setUser({ ...auth.user, - name: `${resp.data.name} ${resp.data.surname}`.trim(), - avatar: avatarUrl, + name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(), }) - setImage(avatarUrl) - keepCurrentAvatar = true - } else { - toast.push(, { - placement: 'top-end', + } + let keepCurrentAvatar = false + + if (hasAvatarChange) { + const avatar = await getCroppedAvatar() + const resp = await putUserAvatar({ + userId: values.id!, + avatar: avatar ? new File([avatar], 'avatar') : undefined, }) + + if (resp.status === 200) { + const avatarUrl = + AVATAR_URL(values.id, values.tenantId) + `?${dayjs().unix()}` + + if (values.id === auth.user.id) { + setUser({ + ...auth.user, + name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(), + avatar: avatarUrl, + }) + } + + setImage(avatarUrl) + setHasAvatarChange(false) + keepCurrentAvatar = true + } else { + const errorMessage = + (resp as { error?: { message?: string } })?.error?.message || 'Hata' + + toast.push(, { + placement: 'top-end', + }) + } } toast.push( @@ -542,7 +571,10 @@ function UserDetails() {
{image && ( @@ -574,6 +606,7 @@ function UserDetails() {
{ setSubmitting(true) @@ -685,6 +718,7 @@ function UserDetails() {
{ setSubmitting(true) @@ -866,6 +900,7 @@ function UserDetails() {
{ setSubmitting(true) @@ -1222,6 +1257,7 @@ function UserDetails() {
{ setSubmitting(true)