From 07eced07ffb2f7bce434b7a6413ee9debfdfc426 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 22 Apr 2018 19:36:08 +0100 Subject: [PATCH] Profile model working --- client/.angular-cli.json | 1 + .../app/components/login/login.component.ts | 28 ++++- .../interceptors/podnoms-api.interceptor.ts | 36 ++++++ client/src/app/models/profile.model.ts | 6 +- client/src/app/services/base.service.ts | 27 +++++ .../src/app/services/podnoms-auth.service.ts | 108 ++++++++++++++++++ client/src/assets/util.js | 9 ++ client/src/facebook-auth.html | 31 +++++ server/Controllers/AccountsController.cs | 2 +- server/Controllers/PodcastController.cs | 32 +++--- server/Controllers/ProfileController.cs | 30 +++-- server/Models/ViewModels/ProfileViewModel.cs | 2 +- .../ViewModels/RegistrationViewModel.cs | 1 - server/Persistence/IUserRepository.cs | 3 +- server/Providers/MappingProvider.cs | 8 ++ server/Startup.cs | 1 + 16 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 client/src/app/interceptors/podnoms-api.interceptor.ts create mode 100644 client/src/app/services/base.service.ts create mode 100644 client/src/app/services/podnoms-auth.service.ts create mode 100644 client/src/assets/util.js create mode 100644 client/src/facebook-auth.html diff --git a/client/.angular-cli.json b/client/.angular-cli.json index 753e179..34af709 100644 --- a/client/.angular-cli.json +++ b/client/.angular-cli.json @@ -11,6 +11,7 @@ "assets", "favicon.ico", "firebase-messaging-sw.js", + "facebook-auth.html", "manifest.json" ], "index": "index.html", diff --git a/client/src/app/components/login/login.component.ts b/client/src/app/components/login/login.component.ts index 55377f9..f3f7d6f 100644 --- a/client/src/app/components/login/login.component.ts +++ b/client/src/app/components/login/login.component.ts @@ -1,6 +1,6 @@ import { PodnomsAuthService } from './../../services/podnoms-auth.service'; -import { AuthService } from 'angularx-social-login'; +import { AuthService, LoginOpt } from 'angularx-social-login'; import { FacebookLoginProvider, GoogleLoginProvider, @@ -16,11 +16,9 @@ import { Subscription } from 'rxjs/Subscription'; styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { - private _authWindow: Window; private _subscription: Subscription; brandNew: boolean = false; - user: any; username: string; password: string; isRequesting: boolean = false; @@ -33,7 +31,6 @@ export class LoginComponent implements OnInit { private _activatedRoute: ActivatedRoute, private _router: Router ) {} - ngOnInit() { this._subscription = this._activatedRoute.queryParams.subscribe( (param: any) => { @@ -45,7 +42,14 @@ export class LoginComponent implements OnInit { login(provider?: string) { this.isRequesting = true; if (provider === 'facebook') { - this._socialAuthService.signIn(FacebookLoginProvider.PROVIDER_ID); + const options: LoginOpt = { + scope: 'email public_profile', + redirect_uri: 'http://localhost:5000/facebook-auth.html' + }; + this._socialAuthService.signIn( + FacebookLoginProvider.PROVIDER_ID, + options + ); } else { this._authService .login(this.username, this.password) @@ -58,7 +62,19 @@ export class LoginComponent implements OnInit { } this._socialAuthService.authState.subscribe((user) => { - this.user = user; + this._authService + .facebookLogin(user.authToken) + .finally(() => (this.isRequesting = false)) + .subscribe( + (result) => { + if (result) { + this._router.navigate(['/']); + } + }, + (error) => { + this.errorMessage = error; + } + ); }); } logout() {} diff --git a/client/src/app/interceptors/podnoms-api.interceptor.ts b/client/src/app/interceptors/podnoms-api.interceptor.ts new file mode 100644 index 0000000..cfdbfd8 --- /dev/null +++ b/client/src/app/interceptors/podnoms-api.interceptor.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { + HttpEvent, + HttpInterceptor, + HttpHandler, + HttpRequest, + HttpHeaderResponse, + HttpHeaders +} from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() +export class PodNomsApiInterceptor implements HttpInterceptor { + private commonHeaders(): HttpHeaders { + const headers = new HttpHeaders({ + 'cache-control': 'no-cache', + 'content-type': 'application/json' + }); + return headers; + } + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + const authToken = localStorage.getItem('auth_token'); + let headers = this.commonHeaders(); + if (authToken) { + headers = headers.append('Authorization', `Bearer ${authToken}`); + } + const changedReq = req.clone({ + headers: headers + }); + return next.handle(changedReq); + } +} diff --git a/client/src/app/models/profile.model.ts b/client/src/app/models/profile.model.ts index 7b219a9..ed965bb 100644 --- a/client/src/app/models/profile.model.ts +++ b/client/src/app/models/profile.model.ts @@ -1,10 +1,12 @@ export class ProfileModel { - id?: number; + id?: string; slug: string; email: string; name: string; description?: string; uid?: string; - apiKey?: string; profileImage?: string; + apiKey: string; + firstName: string; + lastName: string; } diff --git a/client/src/app/services/base.service.ts b/client/src/app/services/base.service.ts new file mode 100644 index 0000000..95efc39 --- /dev/null +++ b/client/src/app/services/base.service.ts @@ -0,0 +1,27 @@ +import { Observable } from 'rxjs/Rx'; + +export abstract class BaseService { + constructor() {} + + protected handleError(error: any) { + const applicationError = error.headers.get('Application-Error'); + + // either applicationError in header or model error in body + if (applicationError) { + return Observable.throw(applicationError); + } + + let modelStateErrors: string = ''; + const serverError = error.json(); + + if (!serverError.type) { + for (let key in serverError) { + if (serverError[key]) + modelStateErrors += serverError[key] + '\n'; + } + } + + modelStateErrors = modelStateErrors = '' ? null : modelStateErrors; + return Observable.throw(modelStateErrors || 'Server error'); + } +} diff --git a/client/src/app/services/podnoms-auth.service.ts b/client/src/app/services/podnoms-auth.service.ts new file mode 100644 index 0000000..289f35f --- /dev/null +++ b/client/src/app/services/podnoms-auth.service.ts @@ -0,0 +1,108 @@ +import { environment } from 'environments/environment'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Observable } from 'rxjs/Rx'; + +import 'rxjs/add/observable/throw'; +import 'rxjs/add/operator/filter'; +import { Headers, RequestOptions } from '@angular/http'; +import { BaseService } from './base.service'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { ProfileModel } from '../models/profile.model'; + +@Injectable() +export class PodnomsAuthService extends BaseService { + private _authNavStatusSource = new BehaviorSubject(false); + authNavStatus$ = this._authNavStatusSource.asObservable(); + private _loggedIn = false; + user: ProfileModel; + httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + constructor(private _router: Router, private _http: HttpClient) { + super(); + this._loggedIn = !!localStorage.getItem('auth_token'); + // ?? not sure if this the best way to broadcast the status but seems to resolve issue on page refresh where auth status is lost in + // header component resulting in authed user nav links disappearing despite the fact user is still logged in + this._authNavStatusSource.next(this._loggedIn); + } + setUser(user: ProfileModel) { + this.user = user; + } + getUser() { + return this.user; + } + isAuthenticated() { + return this._loggedIn; + } + getToken() { + return localStorage.getItem('auth_token'); + } + login(userName, password) { + return this._http + .post( + environment.API_HOST + '/auth/login', + JSON.stringify({ userName, password }), + this.httpOptions + ) + .map((res) => { + localStorage.setItem('auth_token', res['auth_token']); + this._loggedIn = true; + this._authNavStatusSource.next(true); + return true; + }) + .catch(this.handleError); + } + facebookLogin(accessToken: string) { + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + const body = JSON.stringify({ accessToken }); + return this._http + .post( + environment.API_HOST + '/externalauth/facebook', + body, + { + headers + } + ) + .map((res) => { + localStorage.setItem('auth_token', res['auth_token']); + this._loggedIn = true; + this._authNavStatusSource.next(true); + return true; + }) + .catch(this.handleError); + } + public signup(email: string, password: string): Observable { + let body = JSON.stringify({ + email, + password + }); + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) + }; + + let headers = new Headers({ 'Content-Type': 'application/json' }); + let options = new RequestOptions({ headers: headers }); + + return this._http + .post( + environment.API_HOST + '/accounts', + body, + this.httpOptions + ) + .map((res) => true) + .catch(this.handleError); + } + + public logout() { + localStorage.removeItem('auth_token'); + this._router.navigate(['/']); + } + public resetPassword(userName: string) {} + public loginSocial(provider: string): void {} +} diff --git a/client/src/assets/util.js b/client/src/assets/util.js new file mode 100644 index 0000000..79f002a --- /dev/null +++ b/client/src/assets/util.js @@ -0,0 +1,9 @@ +function getParameterByName(name, url) { + if (!url) url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} diff --git a/client/src/facebook-auth.html b/client/src/facebook-auth.html new file mode 100644 index 0000000..6872f73 --- /dev/null +++ b/client/src/facebook-auth.html @@ -0,0 +1,31 @@ + + + + + + + JwtAuthDemo - Facebook Auth + + + + + + + + + diff --git a/server/Controllers/AccountsController.cs b/server/Controllers/AccountsController.cs index 6c1c222..99677c0 100644 --- a/server/Controllers/AccountsController.cs +++ b/server/Controllers/AccountsController.cs @@ -30,7 +30,7 @@ namespace PodNoms.Api.Controllers { // var result = await _userRepository.AddOrUpdate(userIdentity, model.Password); if (!result.Succeeded) return new BadRequestObjectResult(result); - return new OkObjectResult(model); + return new OkObjectResult(model ); } } } \ No newline at end of file diff --git a/server/Controllers/PodcastController.cs b/server/Controllers/PodcastController.cs index d1b0b4d..6492288 100644 --- a/server/Controllers/PodcastController.cs +++ b/server/Controllers/PodcastController.cs @@ -8,6 +8,7 @@ using AutoMapper; using Hangfire; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using PodNoms.Api.Models; @@ -23,17 +24,19 @@ namespace PodNoms.Api.Controllers { public class PodcastController : Controller { private readonly IPodcastRepository _repository; private readonly IUserRepository _userRepository; + private readonly UserManager _userManager; private readonly IOptions _settings; private readonly IMapper _mapper; private ClaimsPrincipal _caller; private readonly IUnitOfWork _uow; - public PodcastController(IPodcastRepository repository, IUserRepository userRepository, + public PodcastController(IPodcastRepository repository, IUserRepository userRepository, UserManager userManager, IOptions options, IMapper mapper, IUnitOfWork unitOfWork, IHttpContextAccessor httpContextAccessor) { - _caller = httpContextAccessor.HttpContext.User; + this._caller = httpContextAccessor.HttpContext.User; this._uow = unitOfWork; this._repository = repository; this._userRepository = userRepository; + this._userManager = userManager; this._settings = options; this._mapper = mapper; } @@ -62,28 +65,25 @@ namespace PodNoms.Api.Controllers { [HttpPost] public async Task Post([FromBody] PodcastViewModel vm) { var userId = _caller.Claims.Single(c => c.Type == "id"); - var user = _userRepository.Get(userId.Value); - if (user == null) - return new BadRequestObjectResult("Unable to look up user profile"); + var user = await this._userManager.FindByIdAsync(userId.Value); + if (user != null) { + if (ModelState.IsValid) { + var item = _mapper.Map(vm); - if (ModelState.IsValid) { - var item = _mapper.Map(vm); - item.User = user; + //remove once we're ready + item.User = _userRepository.Get("fergal.moran@gmail.com"); + item.AppUser = user; - var ret = await _repository.AddOrUpdateAsync(item); - await _uow.CompleteAsync(); - return new OkObjectResult(_mapper.Map(ret)); + var ret = await _repository.AddOrUpdateAsync(item); + await _uow.CompleteAsync(); + return new OkObjectResult(_mapper.Map(ret)); + } } return BadRequest("Invalid request data"); } [HttpPut] public async Task Put([FromBody] PodcastViewModel vm) { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - var user = _userRepository.Get(email); - if (string.IsNullOrEmpty(email) || user == null) - return new BadRequestObjectResult("Unable to look up user profile"); - if (ModelState.IsValid) { var podcast = await _repository.GetAsync(vm.Id); if (podcast != null) { diff --git a/server/Controllers/ProfileController.cs b/server/Controllers/ProfileController.cs index 1624c5f..fbf7d33 100644 --- a/server/Controllers/ProfileController.cs +++ b/server/Controllers/ProfileController.cs @@ -3,43 +3,50 @@ using System.Security.Claims; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using PodNoms.Api.Models; using PodNoms.Api.Models.ViewModels; using PodNoms.Api.Persistence; +using PodNoms.Api.Services.Auth; namespace PodNoms.Api.Controllers { [Authorize] [Route("[controller]")] public class ProfileController : Controller { private IUserRepository _userRepository; + private readonly UserManager _userManager; + public IUnitOfWork _unitOfWork { get; } + + private readonly ClaimsPrincipal _caller; + public IMapper _mapper { get; } - public ProfileController(IUserRepository userRepository, IMapper mapper, IUnitOfWork unitOfWork) { + public ProfileController(IUserRepository userRepository, IMapper mapper, IUnitOfWork unitOfWork, + UserManager userManager, IHttpContextAccessor httpContextAccessor) { + this._caller = httpContextAccessor.HttpContext.User; this._mapper = mapper; this._unitOfWork = unitOfWork; + this._userManager = userManager; this._userRepository = userRepository; } [HttpGet] public async Task> Get() { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - var user = await _userRepository.GetAsync(email); - if (user != null) { - var result = _mapper.Map(user); - return new OkObjectResult(result); - } - return new NotFoundResult(); + var userId = _caller.Claims.Single(c => c.Type == "id"); + var user = await this._userManager.FindByIdAsync(userId.Value); + + var result = _mapper.Map(user); + return new OkObjectResult(result); } [HttpPost] public async Task Post([FromBody] ProfileViewModel item) { + /* TODO: Update this to the new model var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; var user = _userRepository.Get(email); - if (user == null || user.Id != item.Id) - return new UnauthorizedResult(); - user.Id = item.Id; user.EmailAddress = item.Email; user.FullName = item.Name; @@ -48,6 +55,7 @@ namespace PodNoms.Api.Controllers { _userRepository.AddOrUpdate(user); await _unitOfWork.CompleteAsync(); + */ return new OkObjectResult(item); } diff --git a/server/Models/ViewModels/ProfileViewModel.cs b/server/Models/ViewModels/ProfileViewModel.cs index 83f7704..18b46d9 100644 --- a/server/Models/ViewModels/ProfileViewModel.cs +++ b/server/Models/ViewModels/ProfileViewModel.cs @@ -1,6 +1,6 @@ namespace PodNoms.Api.Models.ViewModels { public class ProfileViewModel { - public int Id { get; set; } + public string Id { get; set; } public string Slug { get; set; } public string Email { get; set; } public string Name { get; set; } diff --git a/server/Models/ViewModels/RegistrationViewModel.cs b/server/Models/ViewModels/RegistrationViewModel.cs index 7a856b6..3321d8e 100644 --- a/server/Models/ViewModels/RegistrationViewModel.cs +++ b/server/Models/ViewModels/RegistrationViewModel.cs @@ -4,7 +4,6 @@ namespace PodNoms.Api.Models.ViewModels { public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } - public string Location { get; set; } } } \ No newline at end of file diff --git a/server/Persistence/IUserRepository.cs b/server/Persistence/IUserRepository.cs index 8688ce9..72c2b85 100644 --- a/server/Persistence/IUserRepository.cs +++ b/server/Persistence/IUserRepository.cs @@ -3,7 +3,8 @@ using PodNoms.Api.Models; namespace PodNoms.Api.Persistence { public interface IUserRepository { - User Get(string id); + User Get(int id); + User Get(string email); Task GetAsync(string id); Task GetBySlugAsync(string slug); User UpdateRegistration(string email, string name, string sid, string providerId, string profileImage, string refreshToken); diff --git a/server/Providers/MappingProvider.cs b/server/Providers/MappingProvider.cs index c65a155..433b07b 100644 --- a/server/Providers/MappingProvider.cs +++ b/server/Providers/MappingProvider.cs @@ -33,6 +33,14 @@ namespace PodNoms.Api.Providers { src => src.Name, e => e.MapFrom(m => m.FullName)); + CreateMap() + .ForMember( + src => src.Name, + map => map.MapFrom(s => $"{s.FirstName} {s.LastName}")) + .ForMember( + src => src.ProfileImage, + map => map.MapFrom(s => s.PictureUrl)); + //API Resource to Domain CreateMap() .ForMember(v => v.ImageUrl, map => map.Ignore()) diff --git a/server/Startup.cs b/server/Startup.cs index 6a5bfda..cb96505 100644 --- a/server/Startup.cs +++ b/server/Startup.cs @@ -97,6 +97,7 @@ namespace PodNoms.Api { services.Configure(Configuration.GetSection("Storage")); services.Configure(Configuration.GetSection("ApplicationsSettings")); services.Configure(Configuration.GetSection("EmailSettings")); + services.Configure(Configuration.GetSection("FacebookAuthSettings")); services.Configure(Configuration.GetSection("ImageFileStorageSettings")); services.Configure(Configuration.GetSection("AudioFileStorageSettings")); services.Configure(options => {