From 681b1411fa8d9a33ef238130e3caeca6f93efa69 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 22 Jun 2017 19:42:48 +0200 Subject: [PATCH] Closes #62, Provide fallback image for user profile --- src/Squidex.Read/Users/UserExtensions.cs | 22 +++++++ .../Controllers/Api/Users/Assets/Avatar.png | Bin 0 -> 5857 bytes .../Controllers/Api/Users/UsersController.cs | 61 +++++++++++++++++- src/Squidex/Squidex.csproj | 1 + .../pages/users/users-page.component.html | 2 +- src/Squidex/app/shared/components/pipes.ts | 33 ++++++++-- src/Squidex/app/shared/module.ts | 3 + 7 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/Squidex/Controllers/Api/Users/Assets/Avatar.png 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 0000000000000000000000000000000000000000..bea8668106fa5529c8d6341b1a6a29c136fede2b GIT binary patch literal 5857 zcmV<779Qz|P)w0&+Z?p6C55MH@JhR@Hnc1=LKGI3&dHMaG+21q2`Mo?ZgcAlc)0jWlL%T&wFEJ}G*t7J)>C@gtW9l-8W2XRt zK)|_^mENcTj}Y_244f*fK*STw^gRK1&VkgAd_G^Qtnw7du~2~V@o~Fit9l3|yc6KJ z1E?vtOd3Q0d>gDhliFVUuL~D0WXdhjQCM05u1<%1ih|!|@OuGx$}LZUtbpigft9}- z@^y`uTNo`YsQ}~CbN4BV@DYID3X95EATJR3vqSyfXRS7fw9Jpo%yeaX{!y^<34rb@ zw>$>%G6SC)>F@lGRTRMz3NShqdW4vt0&sV^Wig4%1blqh=ly=U1u3fnymEbEK#-I# zfcdTEl%s4AexLv!JlF5NQa%yNz~#lo3TIq?ih(}@AeBP~3z1>sFU1_fC-3R(OqgA{ z=?XA9IrktD{VjlAv+K4vQvxZ!Kh)p-yqT4nrT~FJz!{gNPcryJ00gt@J!UX5eK}br ze(*wD+m0C(nxX(>GoiDb<*yMiWJY}_fH495E;H5-41W`^Oa|YDEdTUQ;}^t;0($An z)ZBZFuE;P2c!pUV^#{MigsdC)ioN=u0ld?Q z@^3pB|6XOwKHk&QlhnIt-4$SbJY1#NlivXFkltnA-tZ+S(vSLlzHL2A)l~t;W@bFd zO5X+W8a>O^3uZ<#nZiyoiA*X3IV%H15Jdq_hXXE`3r>f<++yb9{}f8U8f^(s>j6h*)*74Y?Tp{Cj( zOS58NjLNp`{pWjnwn{0cBn8+T?LQ0PO{J8h7t9QQU>3{EtGbs>Aar+iptHSI_p*zJ zFFBEZq!cr>qUV>)EU>-itFTDp%h@c(u3k5!@d1Fr2+V~rF%I5R&(Ic;QIi0Aht%1W~gWYZiGb5eOAel;Gb2EZ; z=4dlMkGmNI{XKe!t_w{3=V$x7K2prRMJd4OWax1s{+D9zHG=Dt(+IDvYn~SbLaWD( z))o&cT+W#HO zs3`PaBUoAq!#_Q%dA^~(7QWtYI2?|G%8kc&FflcQSX^UMjtF-RpSIG#a2+I`AL(xo z6>?KSV}L*);6#?c3X3#;I+MZ7oc8m2JK8aH`c%P<5A2To?;1G+PjjRC1Iun!o@O%OeTZn@S5s5cXJcEbo@U1`g%3H z`q09Xg=Pf+gFig#58a>d^?514<;BGcBKjLx#EKXIR#w-cD9WMZk|d$8$E#~yfteDm`bIvv8C(&OsNoo|2p4KL0$^rjLS~}=qpJqompMqP(9w&(FU8Ozny7?%LR|S zN%h>ChAd^|jHAYr*A)Fo1sI#09c17SOP5JMvT_!&X#CJ|0>Q1(NP5uXQ7?NlVyPZr z7vfJ|otf!6>arshfJyiwfPzMrMiWo$fN}V{TU%Rg$lJe9O?5RKD%-`WRO-lG#xhVL zXKkN9>Y|(qFgg`_gn)Tg(s&CqY2y_@%eqLCir$emgNLq62UUA>b+sjW!ccKa#Hq2ipGfIr z0hEhGQhfmGbh-?$yA)W#N7arWQh=+|As+#6HeRISsc@zV>rk^@M0N!`& z!J8D|^5x4mV)`A^L@I%5jm??ONW`!@w>H(wu00+K008g1b&udC1#s3jKMbJ7RFR6L zsi98wxYiC~sW3CvHq_Vr8|u|%u^=t3I?qiDLpLb^A0uDB`np;O!r?DfEEZS&PM6BY zW(0}kVdGn;!;Y#-OW4Qc1%n^E@$Ul#2m}I7fZk@lNW~#Z5}F#-4Kbd5|2H5SE+9=Hn zB0AXMdvy%JeC!ke0NPsAyWrLJO+>b1C6~V(Ud47yy_~kz-&bCFiie2M z$I3kvbQz`g?PX4*K``1-mvw`8XSb6Ct% z378qz{nJ=oQ(yG4+wJJ~9`Cmw00{Us01$Q~P?2SvJ&L8m<;-agJ{6claDFkTCyy-4 zxN>b$v++TM)4pD-Z~f-WFf$X4PlfswW`DV*(TnT;8H880mxF6-s?pQcank^f!3eKz zV0JEuR1TX0i#ddrj3~<860AsQT7$WIKvqja`GwdZwHzh8!$C9i|uHBOT$f#_2}*HvU<=+ z9pGk(fyNW6-v?Cf1$)?z#<0Ayip@v_sap(4D_l-A)YYOTk1#SP2Bt=d2n{N3b|;dW z>Z$|xF=wabx6rVyA56&!rYZnvSctrhKU#`w0R zQfW-i%wlshzXzkVz;Op(S;34k%aR0R@^#&CUVCU56Xm(zvT zmS(iLn@s;qSGobq0yVv?eiVjmo?fxVXH6 z=|C{Ax4x#j3Y{HoXtcomyQ;DR!-E6Zifm(MHmDs~8bB(Y!If(h==Qe5+tF^Kl-e-y zx+H*uEXHn^Z_NxaW&Z~=*A3a9#d z%D%G$v!R@3X)ED1q*7^| z8Ss_WzL1z^1t?0O9F@;6t>kQcK@`y2?Zt)j!`5&78?f1IIPL4j;D8S{N!x8~E3%ET zYZJ(pF}j#Q6c8A=YIeo(_znUAb$>fxcaU}WxnXp)wUoV^ESBcRdfa{2aL$#%Xf%dv z6H_Kibu$Wtabd*g4FE7T*li#MhA}HI zm{qS<_?E7YwiDa<`?Px8=+HOnR|0c4ek9k$!#?cYZ$tbMU* z?h8c``MLf7A1HtWsUHC_F3jd0S(Q+$qIj|gf48Eg0J-)MbYx3P#(up2$AJRur64aj z_iUl4?bqKXZV3mJxd-^>(9qDqqQlKAF04FbbnU9|`fa8G+S6TAeu$@U{NpACSX=1& zJ_t*ORmbXiNchr?2X0b;ix){Irmq+&f!Z@F7D~Np3gyZ##rTH{ z7cSg14Lh{xPK3XQfv=>R51v*Yo|B5Ap#cW>0LJt(ZP15v@Q}My9<}6nswYxT*pS z^>;rH;AJBOku=<}$F5GmKRt_d+T?9flUZHg&@=+E*^CK1&A^N22E0F3JEy5~419`! zZ|NcR^jp>=7X4Vk^u-grKUV${Thvpo0jQN8v zVlSekUIgbCF&k75zT0V| zS67Rw%1TsKxgZJ#*cUKkCz(Pl9!E49Lu5OK_|8tj@dz9a2l{)v40Uf~;EA(7?-!4{ zE-wZ6X(;5XPVy*#{*oojK{lJk^2#damzGPjk|l!6;Y3A+6BSM;oGu6KHal!K5jLA0 zk|=K$smOEEq*`x^=iKPcp$Vqs>4SJ$zyw4Br7^0;8P+0fqFg0?odF+J1G zD2jqNYR&s^rJ%+fn+p8}nE$vqxr$>ak-&0z4dL)A(k5BV=m8O-wx$|wEp9Y6)SEeb z`Lhw9_hb29T#!_Lzu%FB<3#}H3KpwGm>CMH2b+isjRF(Q$u~uWL5_9 z^He1KhJ3pIeF|2~asS+q0(ucZ`I6=^V>=$l)>Z@?o7=l--O9A3EJ+e-YpPLSTZ4v% zIyfDUvd95oO@VO!oUh~PRkj-_s$UwL3cUl&-zfHey^&=Zk!TFDSbX>2&W<6jRyIk3 z%jH6KRRyXlD^OKgiHZuB6&9yh03IFkdB0QWjYUg7<_~@zz!SyZZ!}pYi$o%cWHN)EDF1`0zufF8Q3HnY&HotNrKI8hs)`J%i*wc#IH|}_`Dx2 z=EkBGfSH9WQ=zW`cYr#i1d0Ne+d&j9w-t7Ms#y<|kkp(4)LN z9LY39`QDM<-ojF%s-R0GhJk^$H94a^4B*7%AG9!jQIsFhqwxWq6aWCu5BAJTvheEw zey&%UcR0jlh0^z(>Fq5pBen{<#$p*9>{u7l&Nt!Cz)*J>^t=)sp;HpU>|S*^LjBd31jLdax}c z(q{mEcRA%KAM~ssi0>cjYtxRLZL|dCs`y4;nVx$ /// 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,