diff --git a/src/Squidex.Read/Users/UserExtensions.cs b/src/Squidex.Read/Users/UserExtensions.cs index 8a85567bd..df5e740dc 100644 --- a/src/Squidex.Read/Users/UserExtensions.cs +++ b/src/Squidex.Read/Users/UserExtensions.cs @@ -6,10 +6,13 @@ // All rights reserved. // ========================================================================== +using System; using System.Linq; using Squidex.Core.Identity; using Squidex.Infrastructure; +// ReSharper disable InvertIf + namespace Squidex.Read.Users { public static class UserExtensions @@ -38,5 +41,24 @@ namespace Squidex.Read.Users { return user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexDisplayName)?.Value; } + + public static string PictureNormalizedUrl(this IUser user) + { + var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value; + + if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + { + if (url.Contains("?")) + { + url += "&d=404"; + } + else + { + url += "?d=404"; + } + } + + return url; + } } } diff --git a/src/Squidex/Controllers/Api/Users/Assets/Avatar.png b/src/Squidex/Controllers/Api/Users/Assets/Avatar.png new file mode 100644 index 000000000..bea866810 Binary files /dev/null and b/src/Squidex/Controllers/Api/Users/Assets/Avatar.png differ diff --git a/src/Squidex/Controllers/Api/Users/UsersController.cs b/src/Squidex/Controllers/Api/Users/UsersController.cs index 81813ce30..37da601cb 100644 --- a/src/Squidex/Controllers/Api/Users/UsersController.cs +++ b/src/Squidex/Controllers/Api/Users/UsersController.cs @@ -6,7 +6,10 @@ // All rights reserved. // ========================================================================== +using System.IO; using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -17,18 +20,32 @@ using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; using Squidex.Read.Users; +// ReSharper disable InvertIf + namespace Squidex.Controllers.Api.Users { /// /// Readonly API to retrieve information about squidex users. /// - [Authorize] [ApiExceptionFilter] [SwaggerTag("Users")] public class UsersController : Controller { + private static readonly byte[] AvatarBytes; private readonly UserManager userManager; + static UsersController() + { + var assembly = typeof(UsersController).GetTypeInfo().Assembly; + + using (var avatarStream = assembly.GetManifestResourceStream("Squidex.Controllers.Api.Users.Assets.Avatar.png")) + { + AvatarBytes = new byte[avatarStream.Length]; + + avatarStream.Read(AvatarBytes, 0, AvatarBytes.Length); + } + } + public UsersController(UserManager userManager) { this.userManager = userManager; @@ -44,6 +61,7 @@ namespace Squidex.Controllers.Api.Users /// /// 200 => Users returned. /// + [Authorize] [HttpGet] [Route("users")] [ProducesResponseType(typeof(UserDto[]), 200)] @@ -64,6 +82,7 @@ namespace Squidex.Controllers.Api.Users /// 200 => User found. /// 404 => User not found. /// + [Authorize] [HttpGet] [Route("users/{id}/")] [ProducesResponseType(typeof(UserDto), 200)] @@ -79,6 +98,44 @@ namespace Squidex.Controllers.Api.Users var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName(), PictureUrl = entity.PictureUrl() }); return Ok(response); - } + } + + /// + /// Get user picture by id. + /// + /// The id of the user (GUID). + /// + /// 200 => User found and image or fallback returned. + /// 404 => User not found. + /// + [HttpGet] + [Route("users/{id}/picture")] + [ProducesResponseType(200)] + public async Task GetUserPicture(string id) + { + var entity = await userManager.FindByIdAsync(id); + + if (entity == null) + { + return NotFound(); + } + + using (var client = new HttpClient()) + { + var url = entity.PictureNormalizedUrl(); + + if (!string.IsNullOrWhiteSpace(url)) + { + var response = await client.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + return new FileStreamResult(await response.Content.ReadAsStreamAsync(), response.Content.Headers.ContentType.ToString()); + } + } + } + + return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png"); + } } } diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 597919bd5..49d21c99f 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 0710ebaad..e0771e68f 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -60,7 +60,7 @@ - + {{user.displayName}} diff --git a/src/Squidex/app/shared/components/pipes.ts b/src/Squidex/app/shared/components/pipes.ts index af6542d9c..0b0afa044 100644 --- a/src/Squidex/app/shared/components/pipes.ts +++ b/src/Squidex/app/shared/components/pipes.ts @@ -8,7 +8,9 @@ import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { UsersProviderService } from './../declarations-base'; +import { ApiUrlConfig } from 'framework'; + +import { UserDto, UsersProviderService } from './../declarations-base'; class UserAsyncPipe implements OnDestroy { private lastUserId: string; @@ -122,17 +124,34 @@ export class UserEmailRefPipe extends UserAsyncPipe implements PipeTransform { } } +@Pipe({ + name: 'userDtoPicture', + pure: false +}) +export class UserDtoPicture implements PipeTransform { + constructor( + private readonly apiUrl: ApiUrlConfig + ) { + } + + public transform(user: UserDto): string | null { + return this.apiUrl.buildUrl(`api/users/${user.id}/picture`); + } +} + @Pipe({ name: 'userPicture', pure: false }) export class UserPicturePipe extends UserAsyncPipe implements PipeTransform { - constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) { + constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef, + private readonly apiUrl: ApiUrlConfig + ) { super(users, changeDetector); } public transform(userId: string): string | null { - return super.transformInternal(userId, users => users.getUser(userId).map(u => u.pictureUrl)); + return super.transformInternal(userId, users => users.getUser(userId).map(u => this.apiUrl.buildUrl(`api/users/${u.id}/picture`))); } } @@ -141,16 +160,18 @@ export class UserPicturePipe extends UserAsyncPipe implements PipeTransform { pure: false }) export class UserPictureRefPipe extends UserAsyncPipe implements PipeTransform { - constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef) { + constructor(users: UsersProviderService, changeDetector: ChangeDetectorRef, + private readonly apiUrl: ApiUrlConfig + ) { super(users, changeDetector); } public transform(userId: string): string | null { return super.transformInternal(userId, users => { - const parts = userId.split(':'); + const parts = userId.split(':'); if (parts[0] === 'subject') { - return users.getUser(parts[1]).map(u => u.pictureUrl); + return users.getUser(parts[1]).map(u => this.apiUrl.buildUrl(`api/users/${u.id}/picture`)); } else { return Observable.of('/images/client.png'); } diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 551b8ace5..b2358d815 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -41,6 +41,7 @@ import { SchemasService, ResolveUserGuard, UsagesService, + UserDtoPicture, UserEmailPipe, UserEmailRefPipe, UserNamePipe, @@ -66,6 +67,7 @@ import { HelpComponent, HistoryComponent, LanguageSelectorComponent, + UserDtoPicture, UserEmailPipe, UserEmailRefPipe, UserNamePipe, @@ -80,6 +82,7 @@ import { HelpComponent, HistoryComponent, LanguageSelectorComponent, + UserDtoPicture, UserEmailPipe, UserEmailRefPipe, UserNamePipe,