diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs index 2dd169a..2a13713 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDynamicApiAppService.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Sozsoft.Platform.Extensions; using Microsoft.AspNetCore.Authorization; @@ -10,17 +12,26 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Identity; using Volo.Abp.TenantManagement; using IdentityUser = Volo.Abp.Identity.IdentityUser; +using Sozsoft.Platform.BlobStoring; +using Sozsoft.Platform.Identity; +using Microsoft.Extensions.Configuration; namespace Sozsoft.Platform.ListForms.DynamicApi; [Authorize] public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamicApiAppService { + private static readonly Regex DataUrlRegex = new( + @"^data:(?image\/[a-zA-Z0-9.+-]+);base64,(?.+)$", + RegexOptions.Compiled); + private readonly ITenantRepository tenantRepository; private readonly ITenantManager tenantManager; private readonly IIdentityUserAppService identityUserAppService; private readonly IIdentityRoleAppService identityRoleAppService; private readonly IdentityUserManager userManager; + private readonly BlobManager blobCdnManager; + private readonly IConfiguration configuration; private readonly IOptions identityOptions; public ListFormDynamicApiAppService( @@ -29,6 +40,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic IIdentityUserAppService identityUserAppService, IIdentityRoleAppService identityRoleAppService, IdentityUserManager userManager, + BlobManager blobCdnManager, + IConfiguration configuration, IOptions identityOptions) { this.tenantRepository = tenantRepository; @@ -36,6 +49,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic this.identityUserAppService = identityUserAppService; this.identityRoleAppService = identityRoleAppService; this.userManager = userManager; + this.blobCdnManager = blobCdnManager; + this.configuration = configuration; this.identityOptions = identityOptions; } @@ -44,6 +59,41 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic return Guid.TryParse(value, out var id) ? id : Guid.Empty; } + private async Task SaveAvatarAsync(IdentityUser user, string avatar) + { + if (avatar.IsNullOrWhiteSpace()) + { + return; + } + + var base64 = avatar.Trim(); + var match = DataUrlRegex.Match(base64); + if (match.Success) + { + base64 = match.Groups["data"].Value; + } + else if (avatar.StartsWith("http", StringComparison.OrdinalIgnoreCase) || + avatar.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + byte[] bytes; + try + { + bytes = Convert.FromBase64String(base64); + } + catch (FormatException) + { + return; + } + + var fileName = $"{user.Id}.jpg"; + await using var stream = new MemoryStream(bytes); + await blobCdnManager.SaveAsync(BlobContainerNames.Avatar, fileName, stream); + user.SetAvatar(AvatarProvider.GetAvatar(configuration, user.TenantId?.ToString(), user.Id.ToString())); + } + [Authorize(IdentityPermissions.Users.Create)] public async Task PostUserInsertAsync(DynamicApiBaseInput input) { @@ -68,7 +118,8 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic user.SetDepartmentId(ParseGuid(input.Data.DepartmentId)); user.SetJobPositionId(ParseGuid(input.Data.JobPositionId)); user.SetIsVerified(verify); - user.SetAvatar(input.Data.Avatar); + + await SaveAvatarAsync(user, input.Data.Avatar); (await userManager.CreateAsync(user, input.Data.Password)).CheckErrors(); await userManager.SetLockoutEnabledAsync(user, true); @@ -139,11 +190,7 @@ public class ListFormDynamicApiAppService : PlatformAppService, IListFormDynamic user.SetJobPositionId(ParseGuid(input.Data.JobPositionId)); } - if (input.Data.Avatar != null) - { - user.SetJobPositionId(ParseGuid(input.Data.Avatar)); - } - + await SaveAvatarAsync(user, input.Data.Avatar); (await userManager.UpdateAsync(user)).CheckErrors(); } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index fc16f8e..96e1e24 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -809,7 +809,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 710, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { new () { Order=1,ColCount=1,ColSpan=1,ItemType="group",Items=[ - new EditingFormItemDto { Order=1, DataField="Avatar", ColSpan=1, EditorType2=EditorTypes.dxImageViewer }, + new EditingFormItemDto { Order=1, DataField="Avatar", ColSpan=1, EditorType2=EditorTypes.dxImageViewer, EditorOptions=EditorOptionValues.ImageUploadOptions(false) }, new EditingFormItemDto { Order=2, DataField="Email", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, new EditingFormItemDto { Order=3, DataField="Name", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, new EditingFormItemDto { Order=4, DataField="Surname", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, @@ -2703,7 +2703,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Announcement)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 600, true, true, true, true, false), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 750, 700, true, true, true, true, false), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -2717,7 +2717,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep new EditingFormItemDto { Order = 6, DataField = "PublishDate", ColSpan=1, EditorType2 = EditorTypes.dxDateBox }, new EditingFormItemDto { Order = 7, DataField = "ExpiryDate", ColSpan=1, EditorType2 = EditorTypes.dxDateBox }, new EditingFormItemDto { Order = 8, DataField = "IsPinned", ColSpan=1, EditorType2 = EditorTypes.dxCheckBox }, - new EditingFormItemDto { Order = 9, DataField = "ImageUrl", ColSpan=1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions}, + new EditingFormItemDto { Order = 9, DataField = "ImageUrl", ColSpan=1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions()}, ]} }), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] @@ -4218,7 +4218,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep new EditingFormItemDto { Order = 7, DataField = "Status", ColSpan = 1, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, new EditingFormItemDto { Order = 8, DataField = "ParticipantsCount", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox }, new EditingFormItemDto { Order = 9, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextBox }, - new EditingFormItemDto { Order = 10, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions }, + new EditingFormItemDto { Order = 10, DataField = "Photos", ColSpan = 1, EditorType2 = EditorTypes.dxImageUpload, EditorOptions = EditorOptionValues.ImageUploadOptions() }, ]} }), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 0da7027..f67c5a0 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -27,7 +27,7 @@ public static class PlatformConsts public static string DateFormat = "{ \"format\": \"dd/MM/yyyy\", \"displayFormat\" : \"dd/MM/yyyy\" }"; public static string DateTimeFormat = "{ \"format\": \"dd/MM/yyyy HH:mm\", \"displayFormat\" : \"dd/MM/yyyy HH:mm\" }"; public static string SliderOptions = "{\"tooltip\": { \"enabled\": true }}"; - public static string ImageUploadOptions = "{\"width\": 80, \"height\": 80, \"multiple\": true}"; + public static string ImageUploadOptions(bool multiple = true) => $"{{\"width\": 80, \"height\": 80, \"multiple\": {multiple.ToString().ToLower()}}}"; } public static class EditorScriptValues diff --git a/ui/public/img/others/no-image.png b/ui/public/img/others/no-image.png new file mode 100644 index 0000000..ebb9b8f Binary files /dev/null and b/ui/public/img/others/no-image.png differ diff --git a/ui/src/proxy/admin/models.ts b/ui/src/proxy/admin/models.ts index b58e50a..58c2415 100644 --- a/ui/src/proxy/admin/models.ts +++ b/ui/src/proxy/admin/models.ts @@ -137,6 +137,7 @@ export interface UserInfoViewModel extends ExtensibleObject { phoneNumberConfirmed: boolean accessFailedCount: number shouldChangePasswordOnNextLogin: boolean + avatar: string rocketUsername?: string creationTime: Date | string lastModificationTime: Date | string diff --git a/ui/src/views/form/editors/ImageViewerEditorComponent.tsx b/ui/src/views/form/editors/ImageViewerEditorComponent.tsx index 759073c..d321faf 100644 --- a/ui/src/views/form/editors/ImageViewerEditorComponent.tsx +++ b/ui/src/views/form/editors/ImageViewerEditorComponent.tsx @@ -145,7 +145,7 @@ const ImageViewerEditorComponent = ({ }} onError={({ currentTarget }) => { currentTarget.onerror = null - currentTarget.src = '/img/others/default-profile.png' + currentTarget.src = '/img/others/no-image.png' }} /> diff --git a/ui/src/views/list/editors/ImageViewerEditorComponent.tsx b/ui/src/views/list/editors/ImageViewerEditorComponent.tsx index 7b60b96..85bbdef 100644 --- a/ui/src/views/list/editors/ImageViewerEditorComponent.tsx +++ b/ui/src/views/list/editors/ImageViewerEditorComponent.tsx @@ -189,7 +189,7 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => { }} onError={({ currentTarget }) => { currentTarget.onerror = null - currentTarget.src = '/img/others/default-profile.png' + currentTarget.src = '/img/others/no-image.png' }} /> diff --git a/ui/src/views/list/useListFormColumns.ts b/ui/src/views/list/useListFormColumns.ts index 0acdede..52be303 100644 --- a/ui/src/views/list/useListFormColumns.ts +++ b/ui/src/views/list/useListFormColumns.ts @@ -20,7 +20,7 @@ import { } from '@/proxy/form/models' import { addCss } from './Utils' -const DEFAULT_PROFILE_IMAGE = '/img/others/default-profile.png' +const NO_IMAGE = '/img/others/no-image.png' const cellTemplateMultiValue = ( cellElement: HTMLElement, @@ -92,7 +92,7 @@ function getImgPreview(): HTMLDivElement { ].join(';') const img = document.createElement('img') img.onerror = null - img.src = DEFAULT_PROFILE_IMAGE + img.src = NO_IMAGE img.style.cssText = 'display:block;max-width:312px;max-height:312px;object-fit:contain;border-radius:4px;' el.appendChild(img) @@ -107,7 +107,7 @@ function showImgPreview(src: string, e: MouseEvent) { const imgEl = el.querySelector('img') as HTMLImageElement imgEl.onerror = () => { imgEl.onerror = null - imgEl.src = DEFAULT_PROFILE_IMAGE + imgEl.src = NO_IMAGE } if (imgEl.src !== src) imgEl.src = src @@ -158,7 +158,7 @@ const cellTemplateImage = ( const img = document.createElement('img') img.onerror = () => { img.onerror = null - img.src = DEFAULT_PROFILE_IMAGE + img.src = NO_IMAGE } img.src = url img.alt = ''