diff --git a/client/package.json b/client/package.json index e253528..8e2ca75 100644 --- a/client/package.json +++ b/client/package.json @@ -1,78 +1,79 @@ { - "name": "pod-noms.web", - "version": "0.21.0", - "license": "MIT", - "scripts": { - "ng": "ng", - "start": "ng serve --aot", - "build": "ng build", - "test": "ng test", - "lint": "ng lint" - }, - "ngrxGen": { - "basePath": "./src/app", - "seperateDirectory": true - }, - "private": true, - "dependencies": { - "@angular/animations": "^5.2.10", - "@angular/common": "^5.2.10", - "@angular/compiler": "^5.2.10", - "@angular/core": "^5.2.10", - "@angular/forms": "^5.2.10", - "@angular/http": "^5.2.10", - "@angular/platform-browser": "^5.2.10", - "@angular/platform-browser-dynamic": "^5.2.10", - "@angular/router": "^5.2.10", - "@aspnet/signalr": "1.0.0-rc1-30631", - "@ngrx/effects": "^5.1.0", - "@ngrx/store": "^5.1.0", - "@ngrx/store-devtools": "^5.1.0", - "@qontu/ngx-inline-editor": "^0.2.0-alpha.12", - "angular2-jwt": "^0.2.3", - "angular2-moment": "^1.8.0", - "angularfire2": "^5.0.0-rc.6", - "applicationinsights-js": "^1.0.15", - "auth0": "^2.9.1", - "auth0-lock": "^11.4.0", - "bootstrap": "4.1.0", - "core-js": "^2.5.3", - "dropzone": "^5.3.0", - "firebase": "^4.13.1", - "font-awesome": "^4.7.0", - "howler": "^2.0.9", - "jquery": "^3.3.1", - "lodash": "^4.17.5", - "ng2-toasty": "^4.0.3", - "ngx-bootstrap": "^2.0.4", - "ngx-clipboard": "^10.0.0", - "ngx-moment": "^2.0.0-rc.0", - "popper.js": "^1.13.0", - "rxjs": "5.5.6", - "simple-line-icons": "^2.4.1", - "tether": "^1.4.3", - "uglify-es": "^3.3.10", - "zone.js": "^0.8.20" - }, - "devDependencies": { - "@angular/cli": "1.7.4", - "@angular/compiler-cli": "^5.2.6", - "@angular/language-service": "^5.2.6", - "@types/applicationinsights-js": "^1.0.5", - "@types/jasmine": "^2.8.6", - "@types/node": "~9.4.6", - "codelyzer": "^4.2.1", - "jasmine-core": "~2.99.1", - "jasmine-spec-reporter": "~4.2.1", - "karma": "~2.0.0", - "karma-chrome-launcher": "~2.2.0", - "karma-cli": "~1.0.1", - "karma-coverage-istanbul-reporter": "^1.4.1", - "karma-jasmine": "^1.1.1", - "karma-jasmine-html-reporter": "^0.2.2", - "protractor": "~5.3.0", - "ts-node": "^5.0.1", - "tslint": "~5.9.1", - "typescript": "~2.5.3" - } + "name": "pod-noms.web", + "version": "0.21.0", + "license": "MIT", + "scripts": { + "ng": "ng", + "start": "ng serve --aot", + "build": "ng build", + "test": "ng test", + "lint": "ng lint" + }, + "ngrxGen": { + "basePath": "./src/app", + "seperateDirectory": true + }, + "private": true, + "dependencies": { + "@angular/animations": "^5.2.10", + "@angular/common": "^5.2.10", + "@angular/compiler": "^5.2.10", + "@angular/core": "^5.2.10", + "@angular/forms": "^5.2.10", + "@angular/http": "^5.2.10", + "@angular/platform-browser": "^5.2.10", + "@angular/platform-browser-dynamic": "^5.2.10", + "@angular/router": "^5.2.10", + "@aspnet/signalr": "1.0.0-rc1-30631", + "@ngrx/effects": "^5.1.0", + "@ngrx/store": "^5.1.0", + "@ngrx/store-devtools": "^5.1.0", + "@qontu/ngx-inline-editor": "^0.2.0-alpha.12", + "angular2-jwt": "^0.2.3", + "angular2-moment": "^1.8.0", + "angularfire2": "^5.0.0-rc.6", + "angularx-social-login": "^1.1.8", + "applicationinsights-js": "^1.0.15", + "auth0": "^2.9.1", + "auth0-lock": "^11.4.0", + "bootstrap": "4.1.0", + "core-js": "^2.5.3", + "dropzone": "^5.3.0", + "firebase": "^4.13.1", + "font-awesome": "^4.7.0", + "howler": "^2.0.9", + "jquery": "^3.3.1", + "lodash": "^4.17.5", + "ng2-toasty": "^4.0.3", + "ngx-bootstrap": "^2.0.4", + "ngx-clipboard": "^10.0.0", + "ngx-moment": "^2.0.0-rc.0", + "popper.js": "^1.13.0", + "rxjs": "5.5.6", + "simple-line-icons": "^2.4.1", + "tether": "^1.4.3", + "uglify-es": "^3.3.10", + "zone.js": "^0.8.20" + }, + "devDependencies": { + "@angular/cli": "1.7.4", + "@angular/compiler-cli": "^5.2.6", + "@angular/language-service": "^5.2.6", + "@types/applicationinsights-js": "^1.0.5", + "@types/jasmine": "^2.8.6", + "@types/node": "~9.4.6", + "codelyzer": "^4.2.1", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~2.0.0", + "karma-chrome-launcher": "~2.2.0", + "karma-cli": "~1.0.1", + "karma-coverage-istanbul-reporter": "^1.4.1", + "karma-jasmine": "^1.1.1", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.3.0", + "ts-node": "^5.0.1", + "tslint": "~5.9.1", + "typescript": "~2.5.3" + } } diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 426ee21..d1c581d 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -2,7 +2,7 @@ import { GlobalsService } from './services/globals.service'; import { Component, HostBinding, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { ToastyService } from 'ng2-toasty'; -import { AuthService } from 'app/services/auth.service'; +import { PodnomsAuthService } from 'app/services/podnoms-auth.service'; import { AppInsightsService } from 'app/services/app-insights.service'; import { SignalRService } from 'app/services/signalr.service'; import { ProfileService } from './services/profile.service'; @@ -15,15 +15,13 @@ import { MessagingService } from 'app/services/messaging.service'; }) export class AppComponent implements OnInit { constructor( - private _authService: AuthService, + private _authService: PodnomsAuthService, private _toastyService: ToastyService, private _signalrService: SignalRService, private _profileService: ProfileService, private _messagingService: MessagingService, _appInsights: AppInsightsService ) { - _authService.handleAuthentication(); - _authService.scheduleRenewal(); } loggedIn() { return this._authService.isAuthenticated(); diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index b6be60c..128b822 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -12,10 +12,14 @@ import { ProgressbarModule } from 'ngx-bootstrap/progressbar'; import { AngularFireDatabaseModule } from 'angularfire2/database'; import { AngularFireAuthModule } from 'angularfire2/auth'; import { AngularFireModule } from 'angularfire2'; +import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login'; +import { + GoogleLoginProvider, + FacebookLoginProvider +} from 'angularx-social-login'; import { ModalModule } from 'ngx-bootstrap/modal'; import { AuthGuard } from './services/auth.guard'; -import { AuthConfig, AuthHttp } from 'angular2-jwt'; import { ImageService } from './services/image.service'; import { DebugService } from './services/debug.service'; import { ChatterService } from './services/chatter.service'; @@ -31,7 +35,7 @@ import { AppComponent } from './app.component'; import { HomeComponent } from './components/home/home.component'; import { LoginComponent } from './components/login/login.component'; import { NavbarComponent } from './components/navbar/navbar.component'; -import { AuthService } from './services/auth.service'; +import { PodnomsAuthService } from './services/podnoms-auth.service'; import { ProfileService } from './services/profile.service'; import { MomentModule } from 'angular2-moment'; import { FilterEntryPipe } from './pipes/filter-entry.pipe'; @@ -61,17 +65,21 @@ import { environment } from 'environments/environment'; import { FooterPlayerComponent } from 'app/components/footer-player/footer-player.component'; import { AudioService } from 'app/services/audio.service'; import { HumaniseTimePipe } from './pipes/humanise-time.pipe'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { PodNomsApiInterceptor } from './interceptors/podnoms-api.interceptor'; -export function authHttpServiceFactory(http: Http, options: RequestOptions) { - return new AuthHttp( - new AuthConfig({ - noClientCheck: true, - globalHeaders: [{ 'Content-Type': 'application/json' }], - tokenGetter: () => localStorage.getItem('id_token') - }), - http, - options - ); +let config = new AuthServiceConfig([ + { + id: GoogleLoginProvider.PROVIDER_ID, + provider: new GoogleLoginProvider('Google-OAuth-Client-Id') + }, + { + id: FacebookLoginProvider.PROVIDER_ID, + provider: new FacebookLoginProvider('117715354940616') + } +]); +export function provideConfig() { + return config; } @NgModule({ @@ -112,7 +120,7 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) { }), AngularFireDatabaseModule, AngularFireAuthModule, - + HttpClientModule, AppRoutingModule, HttpModule, FormsModule, @@ -123,6 +131,7 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) { ToastyModule.forRoot(), DropzoneModule, ClipboardModule, + SocialLoginModule, StoreModule.forRoot(reducers), @@ -136,12 +145,16 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) { }) ], providers: [ - AuthService, + PodnomsAuthService, AuthGuard, { - provide: AuthHttp, - useFactory: authHttpServiceFactory, - deps: [Http, RequestOptions] + provide: HTTP_INTERCEPTORS, + useClass: PodNomsApiInterceptor, + multi: true + }, + { + provide: AuthServiceConfig, + useFactory: provideConfig }, SignalRService, ProfileService, diff --git a/client/src/app/components/callback/callback.component.ts b/client/src/app/components/callback/callback.component.ts index 1633e54..d1ef023 100644 --- a/client/src/app/components/callback/callback.component.ts +++ b/client/src/app/components/callback/callback.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { AuthService } from 'app/services/auth.service'; +import { PodnomsAuthService } from 'app/services/podnoms-auth.service'; import { Router } from '@angular/router'; @Component({ @@ -8,7 +8,7 @@ import { Router } from '@angular/router'; styleUrls: ['./callback.component.css'] }) export class CallbackComponent implements OnInit { - constructor(private _authService: AuthService, private _router: Router) {} + constructor(private _authService: PodnomsAuthService, private _router: Router) {} ngOnInit() { this._router.navigate(['/podcasts']); diff --git a/client/src/app/components/home/home.component.ts b/client/src/app/components/home/home.component.ts index d5cf050..c11f43c 100644 --- a/client/src/app/components/home/home.component.ts +++ b/client/src/app/components/home/home.component.ts @@ -1,4 +1,4 @@ -import { AuthService } from 'app/services/auth.service'; +import { PodnomsAuthService } from 'app/services/podnoms-auth.service'; import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @@ -8,7 +8,7 @@ import { Router } from '@angular/router'; styleUrls: ['./home.component.css'] }) export class HomeComponent implements OnInit { - constructor(private _router: Router, private _authService: AuthService) {} + constructor(private _router: Router, private _authService: PodnomsAuthService) {} ngOnInit() { if (this._authService.isAuthenticated) { diff --git a/client/src/app/components/login/login.component.css b/client/src/app/components/login/login.component.css index e69de29..b2a10e5 100644 --- a/client/src/app/components/login/login.component.css +++ b/client/src/app/components/login/login.component.css @@ -0,0 +1,3 @@ +.new-user-alert { + padding-top: 2.5rem; +} diff --git a/client/src/app/components/login/login.component.html b/client/src/app/components/login/login.component.html index ec9130d..7a817d8 100644 --- a/client/src/app/components/login/login.component.html +++ b/client/src/app/components/login/login.component.html @@ -1,8 +1,6 @@ -
+
-
-
\ No newline at end of file +
diff --git a/client/src/app/components/login/login.component.ts b/client/src/app/components/login/login.component.ts index 172a8c1..55377f9 100644 --- a/client/src/app/components/login/login.component.ts +++ b/client/src/app/components/login/login.component.ts @@ -1,32 +1,65 @@ -import { AuthService } from './../../services/auth.service'; +import { PodnomsAuthService } from './../../services/podnoms-auth.service'; + +import { AuthService } from 'angularx-social-login'; +import { + FacebookLoginProvider, + GoogleLoginProvider, + LinkedInLoginProvider +} from 'angularx-social-login'; + import { Component, NgZone, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; @Component({ templateUrl: './login.component.html', 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; signIn; widget; errorMessage: string = ''; - constructor(private _authService: AuthService) {} + constructor( + private _authService: PodnomsAuthService, + private _socialAuthService: AuthService, + private _activatedRoute: ActivatedRoute, + private _router: Router + ) {} - ngOnInit() {} + ngOnInit() { + this._subscription = this._activatedRoute.queryParams.subscribe( + (param: any) => { + this.brandNew = param['brandNew']; + this.username = param['email']; + } + ); + } login(provider?: string) { - if (!provider) { - this._authService.loginUsername( - this.username, - this.password, - success => this.loginSuccess(success), - error => this.loginError(error) - ); + this.isRequesting = true; + if (provider === 'facebook') { + this._socialAuthService.signIn(FacebookLoginProvider.PROVIDER_ID); } else { - this._authService.loginSocial(provider); + this._authService + .login(this.username, this.password) + .finally(() => (this.isRequesting = false)) + .subscribe((result) => { + if (result) { + this._router.navigate(['/']); + } + }, (error) => (this.errorMessage = error)); } + + this._socialAuthService.authState.subscribe((user) => { + this.user = user; + }); } logout() {} loginSuccess(data) { diff --git a/client/src/app/components/navbar/navbar.component.ts b/client/src/app/components/navbar/navbar.component.ts index ec78a43..8cba680 100644 --- a/client/src/app/components/navbar/navbar.component.ts +++ b/client/src/app/components/navbar/navbar.component.ts @@ -1,6 +1,6 @@ import { ProfileModel } from 'app/models/profile.model'; import { Component, OnInit } from '@angular/core'; -import { AuthService } from '../../services/auth.service'; +import { PodnomsAuthService } from '../../services/podnoms-auth.service'; import { ProfileService } from '../../services/profile.service'; import { Observable } from 'rxjs/Observable'; @@ -13,7 +13,7 @@ export class NavbarComponent implements OnInit { user$: Observable; constructor( - private _authService: AuthService, + private _authService: PodnomsAuthService, private _profileService: ProfileService ) {} diff --git a/client/src/app/components/podcast/podcast-add-url-form/podcast-add-url-form.component.ts b/client/src/app/components/podcast/podcast-add-url-form/podcast-add-url-form.component.ts index 72da584..b61a164 100644 --- a/client/src/app/components/podcast/podcast-add-url-form/podcast-add-url-form.component.ts +++ b/client/src/app/components/podcast/podcast-add-url-form/podcast-add-url-form.component.ts @@ -40,16 +40,16 @@ export class PodcastAddUrlFormComponent implements AfterViewInit { this.isPosting = true; const entry = new PodcastEntryModel(this.podcast.id, urlToCheck); this._service.addEntry(entry).subscribe( - e => { + (e) => { if (e) { - if (e.processingStatus == 6) { + if (e.processingStatus == '6') { this.onUploadDeferred.emit(e); } else { this.onUrlAddComplete.emit(e); } } }, - err => { + (err) => { this.isPosting = false; this.errorText = 'This does not look like a valid URL'; this.newEntrySourceUrl = urlToCheck; diff --git a/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts b/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts index 7addf96..858ffa4 100644 --- a/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts +++ b/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts @@ -11,7 +11,7 @@ import { ViewChild } from '@angular/core'; -import { AuthService } from 'app/services/auth.service'; +import { PodnomsAuthService } from 'app/services/podnoms-auth.service'; import { PodcastModel } from 'app/models/podcasts.models'; import { environment } from 'environments/environment'; @@ -29,7 +29,7 @@ export class PodcastUploadFormComponent implements OnInit { constructor( private _toastyService: ToastyService, - private _auth: AuthService + private _auth: PodnomsAuthService ) {} ngOnInit() { const config = { diff --git a/client/src/app/components/podcast/podcast.component.ts b/client/src/app/components/podcast/podcast.component.ts index bc6ffc8..6d192e5 100644 --- a/client/src/app/components/podcast/podcast.component.ts +++ b/client/src/app/components/podcast/podcast.component.ts @@ -25,7 +25,7 @@ export class PodcastComponent { selectedPodcast$: Observable; pendingEntry: PodcastEntryModel = null; entries$: Observable; - uploadMode = true; + uploadMode = false; urlMode = false; firstRun = true; diff --git a/client/src/app/components/register/register.component.html b/client/src/app/components/register/register.component.html index 9d0d261..defff8b 100644 --- a/client/src/app/components/register/register.component.html +++ b/client/src/app/components/register/register.component.html @@ -73,4 +73,4 @@ - \ No newline at end of file + diff --git a/client/src/app/components/register/register.component.ts b/client/src/app/components/register/register.component.ts index 6c99822..056bb4f 100644 --- a/client/src/app/components/register/register.component.ts +++ b/client/src/app/components/register/register.component.ts @@ -1,6 +1,7 @@ import { Observable } from 'rxjs/Observable'; -import { AuthService } from './../../services/auth.service'; +import { PodnomsAuthService } from './../../services/podnoms-auth.service'; import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'app-register', @@ -12,21 +13,23 @@ export class RegisterComponent implements OnInit { password: string; passwordRepeat: string; sending = false; - + _isRequesting: boolean = false; errorMessage: string; - constructor(private _authService: AuthService) {} + constructor(private _authService: PodnomsAuthService, private _router: Router) {} ngOnInit() {} doRegister() { + this._isRequesting = true; this._authService .signup(this.username, this.password) - .catch(err => { - if ((err.code = 'user_exists')) this.errorMessage = 'A user with this email address already exists'; - else this.errorMessage = err.description; - - return Observable.of(`Error logging in: ${err.description}`); - }) - .subscribe(r => console.log('Done')); + .finally(() => (this._isRequesting = false)) + .subscribe((result) => { + if (result) { + this._router.navigate(['/login'], { + queryParams: { brandNew: true, email: this.username } + }); + } + }, (errors) => (this.errorMessage = errors)); } } diff --git a/client/src/app/components/reset/reset.component.ts b/client/src/app/components/reset/reset.component.ts index af04795..e340353 100644 --- a/client/src/app/components/reset/reset.component.ts +++ b/client/src/app/components/reset/reset.component.ts @@ -1,4 +1,4 @@ -import { AuthService } from './../../services/auth.service'; +import { PodnomsAuthService } from './../../services/podnoms-auth.service'; import { Component, OnInit } from '@angular/core'; import 'rxjs/add/operator/catch'; import { Observable } from 'rxjs/Observable'; @@ -12,22 +12,22 @@ export class ResetComponent implements OnInit { username: string; errorMessage: string; successMessage: string; - constructor(private _authService: AuthService) {} + constructor(private _authService: PodnomsAuthService) {} ngOnInit() {} resetPassword() { if (this.username) { this._authService - .resetPassword(this.username) - .catch(err => { - this.errorMessage = err.description; - return Observable.of(`Error resetting password: ${err.description}`); - }) - .subscribe(result => { - console.log('reset.component.ts', 'method', result); - this.errorMessage = ''; - this.successMessage = `A password reset link has been sent to ${this.username}`; - }); + .resetPassword(this.username); + // .catch(err => { + // this.errorMessage = err.description; + // return Observable.of(`Error resetting password: ${err.description}`); + // }) + // .subscribe(result => { + // console.log('reset.component.ts', 'method', result); + // this.errorMessage = ''; + // this.successMessage = `A password reset link has been sent to ${this.username}`; + // }); } else { this.errorMessage = 'Please enter your email address'; } diff --git a/client/src/app/services/auth.guard.ts b/client/src/app/services/auth.guard.ts index 97827eb..61beab1 100644 --- a/client/src/app/services/auth.guard.ts +++ b/client/src/app/services/auth.guard.ts @@ -2,11 +2,11 @@ import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { tokenNotExpired } from 'angular2-jwt'; import { Observable } from 'rxjs/Observable'; -import { AuthService } from './auth.service'; +import { PodnomsAuthService } from './podnoms-auth.service'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private _auth: AuthService) {} + constructor(private _auth: PodnomsAuthService) {} canActivate(route: ActivatedRouteSnapshot): Promise { return new Promise(resolve => { if (this._auth.isAuthenticated()) { diff --git a/client/src/app/services/auth.service.ts b/client/src/app/services/auth.service.ts deleted file mode 100644 index 1e88b53..0000000 --- a/client/src/app/services/auth.service.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { environment } from 'environments/environment'; -import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { AUTH_CONFIG } from './../constants/auth0'; -import * as auth0 from 'auth0-js'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { Observable } from 'rxjs/Rx'; - -import 'rxjs/add/observable/throw'; -import 'rxjs/add/operator/filter'; - -@Injectable() -export class AuthService { - errorMessage: string; - refreshSubscription: any; - - auth0 = new auth0.WebAuth({ - domain: AUTH_CONFIG.AUTH0_DOMAIN, - clientID: AUTH_CONFIG.AUTH0_CLIENT_ID, - redirectUri: AUTH_CONFIG.AUTH0_CALLBACKURL, - audience: `https://${AUTH_CONFIG.AUTH0_DOMAIN}/userinfo`, - responseType: 'token id_token', - prompt: 'select_account', - scope: 'openid profile email' - }); - constructor(private _router: Router) {} - public loginUsername(username: string, password: string, success, error): void { - this.auth0.client.login( - { - realm: 'podnoms-db-connection', - username: username, - password: password - }, - (err, authResult) => { - if (err) { - error(err); - console.log(err); - return; - } else if (authResult && authResult.accessToken && authResult.idToken) { - this.setSession(authResult); - success(authResult); - } - } - ); - } - public signup(email: string, password: string): Observable { - return Observable.create(observer => { - this.auth0.redirect.signupAndLogin( - { - connection: 'podnoms-db-connection', - email, - password - }, - err => { - if (err) { - observer.error(err); - } else observer.next(); - } - ); - }); - } - public resetPassword(email: string): Observable { - return Observable.create(observer => { - this.auth0.changePassword( - { - connection: 'podnoms-db-connection', - email - }, - (err, resp) => { - if (err) { - console.error(err); - Observable.throw(err); - } else { - observer.next('success'); - } - } - ); - }); - } - public loginSocial(provider: string): void { - this.auth0.authorize({ - connection: provider - }); - } - public handleAuthentication(): void { - this.auth0.parseHash((err, authResult) => { - if (authResult && authResult.accessToken && authResult.idToken) { - this.setSession(authResult); - } else if (err) { - this.logout(); - this._router.navigate(['/']).then(r => window.location.reload()); // TODO: Remove this for the love of baby Jesus! - console.log(err); - } - }); - } - public getToken(): string { - if (this.isAuthenticated()) return localStorage.getItem('id_token'); - return ''; - } - public renewToken() { - this.auth0.renewAuth( - { - audience: 'https://podnoms/', - redirectUri: `${environment.API_HOST}/silent`, - usePostMessage: true, - postMessageOrigin: environment.BASE_URL - }, - (err, result) => { - if (err) { - console.log(err); - } else { - this.setSession(result); - } - } - ); - } - public scheduleRenewal() { - if (!this.isAuthenticated()) return; - this.unscheduleRenewal(); - - const expiresAt = JSON.parse(window.localStorage.getItem('expires_at')); - const source = Observable.of(expiresAt).flatMap(e => { - const now = Date.now(); - return Observable.timer(Math.max(1, e - now)); - }); - this.refreshSubscription = source.subscribe(() => { - console.log('auth.service.ts', 'scheduleRenewal', 'Starting renewal and schedule'); - this.renewToken(); - this.scheduleRenewal(); - }); - } - - public unscheduleRenewal() { - if (!this.refreshSubscription) return; - this.refreshSubscription.unsubscribe(); - } - private setSession(authResult): void { - const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime()); - localStorage.setItem('access_token', authResult.accessToken); - localStorage.setItem('id_token', authResult.idToken); - localStorage.setItem('expires_at', expiresAt); - this.scheduleRenewal(); - this._router.navigate(['/']); - } - public logout(): void { - localStorage.removeItem('access_token'); - localStorage.removeItem('id_token'); - localStorage.removeItem('expires_at'); - - this._router.navigate(['/']); - window.location.reload(); - } - public isAuthenticated(): boolean { - const expiresAt = JSON.parse(localStorage.getItem('expires_at')); - return new Date().getTime() < expiresAt; - } -} diff --git a/client/src/app/services/chatter.service.ts b/client/src/app/services/chatter.service.ts index 81b3d1f..429c8e9 100644 --- a/client/src/app/services/chatter.service.ts +++ b/client/src/app/services/chatter.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; import { environment } from 'environments/environment'; -import { AuthHttp } from 'angular2-jwt'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class ChatterService { - constructor(private _http: AuthHttp) {} + constructor(private _http: HttpClient) {} ping(message: string): any { return this._http.post( diff --git a/client/src/app/services/debug.service.ts b/client/src/app/services/debug.service.ts index 1f371c8..1d4a659 100644 --- a/client/src/app/services/debug.service.ts +++ b/client/src/app/services/debug.service.ts @@ -1,24 +1,29 @@ import { environment } from 'environments/environment'; import { Observable } from 'rxjs/Observable'; -import { AuthHttp } from 'angular2-jwt'; import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class DebugService { - constructor(private _http: AuthHttp) {} + constructor(private _http: HttpClient) {} sendRealtime(message: string): any { - return this._http.post(environment.API_HOST + '/debug/realtime', JSON.stringify(message)); + return this._http.post( + environment.API_HOST + '/debug/realtime', + JSON.stringify(message) + ); } getDebugInfo(): Observable { - return this._http.get(environment.API_HOST + '/debug').map(r => r.json()); + return this._http.get(environment.API_HOST + '/debug'); } ping(): Observable { - return this._http.get(environment.API_HOST + '/ping').map(r => r.text()); + return this._http.get(environment.API_HOST + '/ping'); } - sendPush(): Observable{ - return this._http.get(environment.API_HOST + '/debug/serverpush').map(r => r.text()); + sendPush(): Observable { + return this._http.get( + environment.API_HOST + '/debug/serverpush' + ); } } diff --git a/client/src/app/services/entries.service.ts b/client/src/app/services/entries.service.ts index 332c671..5b1b84e 100644 --- a/client/src/app/services/entries.service.ts +++ b/client/src/app/services/entries.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class EntriesService { - constructor(private http: Http) {} + constructor(private http: HttpClient) {} get(): Observable { return this.http.get('https://api.com'); diff --git a/client/src/app/services/image.service.ts b/client/src/app/services/image.service.ts index 263c2a7..68d7667 100644 --- a/client/src/app/services/image.service.ts +++ b/client/src/app/services/image.service.ts @@ -1,13 +1,12 @@ import {Http, Headers} from '@angular/http'; import {Injectable} from '@angular/core'; -import {AuthService} from './auth.service'; +import {PodnomsAuthService} from './podnoms-auth.service'; import { environment } from 'environments/environment'; @Injectable() export class ImageService { - // TODO: Change this to use AuthHttp when I can figure out why formData is null - constructor(private _http: Http, private _auth: AuthService) { + constructor(private _http: Http, private _auth: PodnomsAuthService) { } upload(podcastSlug: string, image) { diff --git a/client/src/app/services/jobs.service.ts b/client/src/app/services/jobs.service.ts index 0c6af1b..d1f27be 100644 --- a/client/src/app/services/jobs.service.ts +++ b/client/src/app/services/jobs.service.ts @@ -1,21 +1,26 @@ import { Injectable } from '@angular/core'; import { Response } from '@angular/http'; -import { AuthHttp } from 'angular2-jwt'; import { Observable } from 'rxjs/Observable'; import { environment } from 'environments/environment'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class JobsService { - constructor(private _http: AuthHttp) { } + constructor(private _http: HttpClient) {} processOrphans(): Observable { - return this._http.get(environment.API_HOST + '/job/processorphans'); + return this._http.get( + environment.API_HOST + '/job/processorphans' + ); } - processPlaylists(): Observable { - return this._http.get(environment.API_HOST + '/job/processplaylists'); + return this._http.get( + environment.API_HOST + '/job/processplaylists' + ); } updateYouTubeDl(): Observable { - return this._http.get(environment.API_HOST + '/job/updateyoutubedl'); + return this._http.get( + environment.API_HOST + '/job/updateyoutubedl' + ); } } diff --git a/client/src/app/services/podcast.service.ts b/client/src/app/services/podcast.service.ts index a186a76..31988d9 100644 --- a/client/src/app/services/podcast.service.ts +++ b/client/src/app/services/podcast.service.ts @@ -1,9 +1,9 @@ import { environment } from 'environments/environment'; import { PodcastEntryModel } from 'app/models/podcasts.models'; import { PodcastModel } from './../models/podcasts.models'; -import { AuthHttp } from 'angular2-jwt'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class PodcastService { @@ -13,24 +13,25 @@ export class PodcastService { } return value; } - constructor(private _http: AuthHttp) { } + constructor(private _http: HttpClient) {} //#region Podcasts get(): Observable { - return this._http - .get(environment.API_HOST + '/podcast/') - .map(res => res.json()); + return this._http.get( + environment.API_HOST + '/podcast/' + ); } getPodcast(slug: string): Observable { - return this._http - .get(environment.API_HOST + '/podcast/' + slug) - .map(res => res.json()); + return this._http.get( + environment.API_HOST + '/podcast/' + slug + ); } addPodcast(podcast: PodcastModel): Observable { console.log('PodcastService', 'addPodcast', podcast); const data = JSON.stringify(podcast, PodcastService._replacer); - return this._http - .post(environment.API_HOST + '/podcast', data) - .map(res => res.json()); + return this._http.post( + environment.API_HOST + '/podcast', + data + ); } updatePodcast(podcast: PodcastModel) { return this._http.put(environment.API_HOST + '/podcast/', podcast); @@ -41,43 +42,48 @@ export class PodcastService { //#endregion //#region Entries getEntries(slug: string): any { - return this._http - .get(environment.API_HOST + '/entry/all/' + slug) - .map(res => res.json()); + return this._http.get(environment.API_HOST + '/entry/all/' + slug); } - addEntry(entry: PodcastEntryModel) { - return this._http - .post(environment.API_HOST + '/entry', JSON.stringify(entry)) - .map(res => res.json()); + addEntry(entry: PodcastEntryModel): Observable { + return this._http.post( + environment.API_HOST + '/entry', + JSON.stringify(entry) + ); } updateEntry(entry: PodcastEntryModel) { - return this._http - .post(environment.API_HOST + '/entry', JSON.stringify(entry)) - .map(res => res.json()); + return this._http.post( + environment.API_HOST + '/entry', + JSON.stringify(entry) + ); } deleteEntry(id: number) { return this._http.delete(environment.API_HOST + '/entry/' + id); } checkEntry(url: string): Observable { return this._http - .post(environment.API_HOST + '/entry/isvalid/', `"${url}"`) - .map(r => (r.status == 200 ? true : false)) + .post( + environment.API_HOST + '/entry/isvalid/', + `"${url}"` + ) + .map((r) => (r.status == 200 ? true : false)) .catch((error: any) => { return Observable.throw(new Error(error.status)); }); } reSubmitEntry(entry: PodcastEntryModel): Observable { - return this._http - .post(environment.API_HOST + '/entry/resubmit', entry) - .map(res => res.json()); + return this._http.post( + environment.API_HOST + '/entry/resubmit', + entry + ); } //#endregion //#region Playlists addPlaylist(entry: PodcastEntryModel) { - return this._http - .post(environment.API_HOST + '/playlist', JSON.stringify(entry)) - .map(res => res.json()); + return this._http.post( + environment.API_HOST + '/playlist', + JSON.stringify(entry) + ); } //#endregion } diff --git a/client/src/app/services/profile.service.ts b/client/src/app/services/profile.service.ts index 4582255..b13d706 100644 --- a/client/src/app/services/profile.service.ts +++ b/client/src/app/services/profile.service.ts @@ -2,21 +2,21 @@ import { environment } from 'environments/environment'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { ProfileModel } from 'app/models/profile.model'; -import { AuthHttp } from 'angular2-jwt'; import 'rxjs/add/operator/map'; import { Profile } from 'selenium-webdriver/firefox'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class ProfileService { - constructor(private _http: AuthHttp) {} profile: ProfileModel; + constructor(private _http: HttpClient) {} getProfile(): Observable { if (!this.profile) { return this._http - .get(environment.API_HOST + '/profile') - .map(res => { - this.profile = res.json(); + .get(environment.API_HOST + '/profile') + .map((res) => { + this.profile = res; return this.profile; }); } else { @@ -26,20 +26,22 @@ export class ProfileService { updateProfile(profile): Observable { console.log('ProfileService', 'updateProfile', profile); - return this._http - .post(environment.API_HOST + '/profile', profile) - .map(res => res.json()); + return this._http.post( + environment.API_HOST + '/profile', + profile + ); } checkSlug(slug): Observable { console.log('profile.service.ts', 'checkSlug', slug); - return this._http - .get(environment.API_HOST + '/profile/checkslug/' + slug) - .map(res => res.text()); + return this._http.get( + environment.API_HOST + '/profile/checkslug/' + slug + ); } regenerateApiKey(): Observable { - return this._http - .post(environment.API_HOST + '/profile/updateapikey', null) - .map(res => res.text()); + return this._http.post( + environment.API_HOST + '/profile/updateapikey', + null + ); } } diff --git a/client/src/app/services/push-registration.service.ts b/client/src/app/services/push-registration.service.ts index 303bdfa..5c423af 100644 --- a/client/src/app/services/push-registration.service.ts +++ b/client/src/app/services/push-registration.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { AuthHttp } from 'angular2-jwt'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; @@ -7,11 +6,12 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/observable/throw'; import { environment } from 'environments/environment'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class PushRegistrationService { private API_URL: string; - constructor(private http: AuthHttp) { + constructor(private _http: HttpClient) { this.API_URL = environment.API_HOST; } @@ -30,7 +30,7 @@ export class PushRegistrationService { addSubscriber(subscription) { const url = `${this.API_URL}/webpush/subscribe`; - return this.http.post(url, subscription).catch(this.handleError); + return this._http.post(url, subscription).catch(this.handleError); } deleteSubscriber(subscription) { @@ -40,7 +40,9 @@ export class PushRegistrationService { subscription: subscription }; - return this.http.post(url, body).catch(this.handleError); + return this._http + .post(url, JSON.stringify(body)) + .catch(this.handleError); } private handleError(error: Response | any) { diff --git a/client/src/app/services/signalr.service.ts b/client/src/app/services/signalr.service.ts index a50fd77..5138d6e 100644 --- a/client/src/app/services/signalr.service.ts +++ b/client/src/app/services/signalr.service.ts @@ -1,4 +1,4 @@ -import { AuthService } from './auth.service'; +import { PodnomsAuthService } from './podnoms-auth.service'; import { Injectable } from '@angular/core'; import { HubConnection, @@ -12,7 +12,7 @@ import { environment } from 'environments/environment'; export class SignalRService { public connection: HubConnection; - constructor(private _auth: AuthService) {} + constructor(private _auth: PodnomsAuthService) {} public init(hub: string): Promise { const url = `${environment.SIGNALR_HOST}/hubs/${hub}`; diff --git a/server/Controllers/AccountsController.cs b/server/Controllers/AccountsController.cs new file mode 100644 index 0000000..6c1c222 --- /dev/null +++ b/server/Controllers/AccountsController.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using PodNoms.Api.Models.ViewModels; +using PodNoms.Api.Persistence; +using PodNoms.Api.Services.Auth; + +namespace PodNoms.Api.Controllers { + + [Route("[controller]")] + public class AccountsController : Controller { + private readonly IUserRepository _userRepository; + private readonly UserManager _userManager; + private readonly IMapper _mapper; + + public AccountsController(IUserRepository userRepository, UserManager userManager, IMapper mapper) { + this._userRepository = userRepository; + this._userManager = userManager; + this._mapper = mapper; + } + // POST api/accounts + [HttpPost] + public async Task Post([FromBody]RegistrationViewModel model) { + if (!ModelState.IsValid) { + return BadRequest(ModelState); + } + var userIdentity = _mapper.Map(model); + var result = await _userManager.CreateAsync(userIdentity, model.Password); + // var result = await _userRepository.AddOrUpdate(userIdentity, model.Password); + + if (!result.Succeeded) return new BadRequestObjectResult(result); + return new OkObjectResult(model); + } + } +} \ No newline at end of file diff --git a/server/Controllers/AuthController.cs b/server/Controllers/AuthController.cs index 9c13f2d..bb9d5ba 100644 --- a/server/Controllers/AuthController.cs +++ b/server/Controllers/AuthController.cs @@ -1,31 +1,60 @@ -using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; using PodNoms.Api.Models; -using PodNoms.Api.Persistence; +using PodNoms.Api.Models.ViewModels; +using PodNoms.Api.Services.Auth; +using PodNoms.Api.Utils; namespace PodNoms.Api.Controllers { - [Authorize] + [Route("[controller]")] public class AuthController : Controller { - protected IUserRepository _userRepository { get; } + private readonly UserManager _userManager; + private readonly IJwtFactory _jwtFactory; + private readonly JwtIssuerOptions _jwtOptions; - public AuthController(IUserRepository repository) { - this._userRepository = repository; + public AuthController(UserManager userManager, IJwtFactory jwtFactory, IOptions jwtOptions) { + _userManager = userManager; + _jwtFactory = jwtFactory; + _jwtOptions = jwtOptions.Value; } - protected async Task GetUserAsync() { - var identifier = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - var user = await this._userRepository.GetAsync(identifier); - return user; + + // POST api/auth/login + [HttpPost("login")] + public async Task Post([FromBody]CredentialsViewModel credentials) { + if (!ModelState.IsValid) { + return BadRequest(ModelState); + } + + var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password); + if (identity == null) { + return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState)); + } + + var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, + new JsonSerializerSettings { Formatting = Formatting.Indented }); + return new OkObjectResult(jwt); } - protected async Task GetUserUidAsync() { - var user = await GetUserAsync(); - return user.Uid; - } - protected async Task GetUserIdAsync() { - var user = await GetUserAsync(); - return user.Id; + + private async Task GetClaimsIdentity(string userName, string password) { + if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) + return await Task.FromResult(null); + + // get the user to verifty + var userToVerify = await _userManager.FindByNameAsync(userName); + + if (userToVerify == null) return await Task.FromResult(null); + + // check the credentials + if (await _userManager.CheckPasswordAsync(userToVerify, password)) { + return await Task.FromResult(_jwtFactory.GenerateClaimsIdentity(userName, userToVerify.Id)); + } + + // Credentials are invalid, or account doesn't exist + return await Task.FromResult(null); } } -} \ No newline at end of file +} diff --git a/server/Controllers/DebugController.cs b/server/Controllers/DebugController.cs index b57fa2e..dffcc81 100644 --- a/server/Controllers/DebugController.cs +++ b/server/Controllers/DebugController.cs @@ -18,7 +18,7 @@ using WebPush = Lib.Net.Http.WebPush; namespace PodNoms.Api.Controllers { [Route("[controller]")] - public class DebugController : AuthController { + public class DebugController : UserController { private readonly StorageSettings _storageSettings; private readonly AudioFileStorageSettings _audioFileStorageSettings; private readonly ApplicationsSettings _applicationsSettings; diff --git a/server/Controllers/EntryController.cs b/server/Controllers/EntryController.cs index 251adb6..a5b63b0 100644 --- a/server/Controllers/EntryController.cs +++ b/server/Controllers/EntryController.cs @@ -19,7 +19,7 @@ using PodNoms.Api.Services.Storage; namespace PodNoms.Api.Controllers { [Route("[controller]")] - public class EntryController : AuthController { + public class EntryController : UserController { private readonly IPodcastRepository _podcastRepository; private readonly IEntryRepository _repository; private readonly IUnitOfWork _unitOfWork; diff --git a/server/Controllers/PodcastController.cs b/server/Controllers/PodcastController.cs index 4000a5d..d1b0b4d 100644 --- a/server/Controllers/PodcastController.cs +++ b/server/Controllers/PodcastController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using AutoMapper; using Hangfire; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using PodNoms.Api.Models; @@ -24,10 +25,12 @@ namespace PodNoms.Api.Controllers { private readonly IUserRepository _userRepository; private readonly IOptions _settings; private readonly IMapper _mapper; + private ClaimsPrincipal _caller; private readonly IUnitOfWork _uow; public PodcastController(IPodcastRepository repository, IUserRepository userRepository, - IOptions options, IMapper mapper, IUnitOfWork unitOfWork) { + IOptions options, IMapper mapper, IUnitOfWork unitOfWork, IHttpContextAccessor httpContextAccessor) { + _caller = httpContextAccessor.HttpContext.User; this._uow = unitOfWork; this._repository = repository; this._userRepository = userRepository; @@ -37,12 +40,10 @@ namespace PodNoms.Api.Controllers { [HttpGet] public async Task> Get() { - var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - if (!string.IsNullOrEmpty(email)) { - var podcasts = await _repository.GetAllAsync(email); - var ret = _mapper.Map, List>(podcasts.ToList()); - return ret; - } + var userId = _caller.Claims.Single(c => c.Type == "id"); + var podcasts = await _repository.GetAllAsync(userId.Value); + var ret = _mapper.Map, List>(podcasts.ToList()); + return ret; throw new Exception("No local user stored!"); } @@ -60,9 +61,9 @@ namespace PodNoms.Api.Controllers { [HttpPost] public async Task Post([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) + 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"); if (ModelState.IsValid) { diff --git a/server/Controllers/UserController.cs b/server/Controllers/UserController.cs new file mode 100644 index 0000000..3505278 --- /dev/null +++ b/server/Controllers/UserController.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PodNoms.Api.Models; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Controllers { + [Authorize] + [Obsolete("This should be superceded by the new identity stuff")] + public class UserController : Controller { + protected IUserRepository _userRepository { get; } + + public UserController(IUserRepository repository) { + this._userRepository = repository; + } + protected async Task GetUserAsync() { + var identifier = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + var user = await this._userRepository.GetAsync(identifier); + return user; + } + protected async Task GetUserUidAsync() { + var user = await GetUserAsync(); + return user.Uid; + } + protected async Task GetUserIdAsync() { + var user = await GetUserAsync(); + return user.Id; + } + } +} \ No newline at end of file diff --git a/server/Controllers/WebPushController.cs b/server/Controllers/WebPushController.cs index de364b0..fc7c66c 100644 --- a/server/Controllers/WebPushController.cs +++ b/server/Controllers/WebPushController.cs @@ -10,7 +10,7 @@ namespace PodNoms.Api.Controllers { // [Authorize] [Route("[controller]")] - public class WebPushController : AuthController { + public class WebPushController : UserController { private readonly IPushSubscriptionStore _subscriptionStore; public readonly IPushNotificationService _notificationService; diff --git a/server/Migrations/20180421161830_AddedAuthTables.Designer.cs b/server/Migrations/20180421161830_AddedAuthTables.Designer.cs new file mode 100644 index 0000000..8794cc7 --- /dev/null +++ b/server/Migrations/20180421161830_AddedAuthTables.Designer.cs @@ -0,0 +1,194 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180421161830_AddedAuthTables")] + partial class AddedAuthTables + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(50); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180421161830_AddedAuthTables.cs b/server/Migrations/20180421161830_AddedAuthTables.cs new file mode 100644 index 0000000..de95779 --- /dev/null +++ b/server/Migrations/20180421161830_AddedAuthTables.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class AddedAuthTables : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/server/Migrations/20180421162833_ChangeContextType.Designer.cs b/server/Migrations/20180421162833_ChangeContextType.Designer.cs new file mode 100644 index 0000000..e3128ee --- /dev/null +++ b/server/Migrations/20180421162833_ChangeContextType.Designer.cs @@ -0,0 +1,406 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180421162833_ChangeContextType")] + partial class ChangeContextType + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(50); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PodNoms.Api.Services.Auth.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FacebookId"); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("PictureUrl"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180421162833_ChangeContextType.cs b/server/Migrations/20180421162833_ChangeContextType.cs new file mode 100644 index 0000000..63efaa1 --- /dev/null +++ b/server/Migrations/20180421162833_ChangeContextType.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class ChangeContextType : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + FirstName = table.Column(nullable: true), + LastName = table.Column(nullable: true), + FacebookId = table.Column(nullable: true), + PictureUrl = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/server/Migrations/20180421215724_RenameUserTable.Designer.cs b/server/Migrations/20180421215724_RenameUserTable.Designer.cs new file mode 100644 index 0000000..f8d2283 --- /dev/null +++ b/server/Migrations/20180421215724_RenameUserTable.Designer.cs @@ -0,0 +1,406 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180421215724_RenameUserTable")] + partial class RenameUserTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(50); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("UserDetails"); + }); + + modelBuilder.Entity("PodNoms.Api.Services.Auth.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FacebookId"); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("PictureUrl"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180421215724_RenameUserTable.cs b/server/Migrations/20180421215724_RenameUserTable.cs new file mode 100644 index 0000000..16816c2 --- /dev/null +++ b/server/Migrations/20180421215724_RenameUserTable.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class RenameUserTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Podcasts_Users_UserId", + table: "Podcasts"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "UserDetails"); + + migrationBuilder.RenameIndex( + name: "IX_Users_Slug", + table: "UserDetails", + newName: "IX_UserDetails_Slug"); + + migrationBuilder.AddPrimaryKey( + name: "PK_UserDetails", + table: "UserDetails", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Podcasts_UserDetails_UserId", + table: "Podcasts", + column: "UserId", + principalTable: "UserDetails", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Podcasts_UserDetails_UserId", + table: "Podcasts"); + + migrationBuilder.DropPrimaryKey( + name: "PK_UserDetails", + table: "UserDetails"); + + migrationBuilder.RenameTable( + name: "UserDetails", + newName: "Users"); + + migrationBuilder.RenameIndex( + name: "IX_UserDetails_Slug", + table: "Users", + newName: "IX_Users_Slug"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Podcasts_Users_UserId", + table: "Podcasts", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/server/Migrations/20180422013049_JiggleUserModel.Designer.cs b/server/Migrations/20180422013049_JiggleUserModel.Designer.cs new file mode 100644 index 0000000..5ca2cb5 --- /dev/null +++ b/server/Migrations/20180422013049_JiggleUserModel.Designer.cs @@ -0,0 +1,414 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using PodNoms.Api.Persistence; + +namespace PodNoms.Api.Migrations +{ + [DbContext(typeof(PodnomsDbContext))] + [Migration("20180422013049_JiggleUserModel")] + partial class JiggleUserModel + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CreateDate"); + + b.Property("PodcastId"); + + b.Property("SourceUrl"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PodcastId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AppUserId"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("Slug") + .IsUnicode(true); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("UserId"); + + b.ToTable("Podcasts"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AudioFileSize"); + + b.Property("AudioLength"); + + b.Property("AudioUrl"); + + b.Property("Author"); + + b.Property("CreateDate") + .ValueGeneratedOnAdd() + .HasDefaultValueSql("getdate()"); + + b.Property("Description"); + + b.Property("ImageUrl"); + + b.Property("PlaylistId"); + + b.Property("PodcastId"); + + b.Property("Processed"); + + b.Property("ProcessingPayload"); + + b.Property("ProcessingStatus"); + + b.Property("SourceUrl"); + + b.Property("Title"); + + b.Property("Uid"); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PodcastId"); + + b.ToTable("PodcastEntries"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiKey") + .HasMaxLength(50); + + b.Property("CreateDate"); + + b.Property("EmailAddress") + .HasMaxLength(100); + + b.Property("FullName") + .HasMaxLength(100); + + b.Property("ProfileImage"); + + b.Property("ProviderId") + .HasMaxLength(50); + + b.Property("RefreshToken"); + + b.Property("Sid") + .HasMaxLength(50); + + b.Property("Slug") + .HasMaxLength(50); + + b.Property("Uid") + .HasMaxLength(50); + + b.Property("UpdateDate"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique() + .HasFilter("[Slug] IS NOT NULL"); + + b.ToTable("UserDetails"); + }); + + modelBuilder.Entity("PodNoms.Api.Services.Auth.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FacebookId"); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("PictureUrl"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => + { + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany() + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId"); + + b.HasOne("PodNoms.Api.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("PodNoms.Api.Models.PodcastEntry", b => + { + b.HasOne("PodNoms.Api.Models.Playlist") + .WithMany("PodcastEntries") + .HasForeignKey("PlaylistId"); + + b.HasOne("PodNoms.Api.Models.Podcast", "Podcast") + .WithMany("PodcastEntries") + .HasForeignKey("PodcastId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/Migrations/20180422013049_JiggleUserModel.cs b/server/Migrations/20180422013049_JiggleUserModel.cs new file mode 100644 index 0000000..186e200 --- /dev/null +++ b/server/Migrations/20180422013049_JiggleUserModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace PodNoms.Api.Migrations +{ + public partial class JiggleUserModel : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AppUserId", + table: "Podcasts", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Podcasts_AppUserId", + table: "Podcasts", + column: "AppUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Podcasts_AspNetUsers_AppUserId", + table: "Podcasts", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Podcasts_AspNetUsers_AppUserId", + table: "Podcasts"); + + migrationBuilder.DropIndex( + name: "IX_Podcasts_AppUserId", + table: "Podcasts"); + + migrationBuilder.DropColumn( + name: "AppUserId", + table: "Podcasts"); + } + } +} diff --git a/server/Migrations/PodnomsDbContextModelSnapshot.cs b/server/Migrations/PodnomsDbContextModelSnapshot.cs index 81aed92..6d10001 100644 --- a/server/Migrations/PodnomsDbContextModelSnapshot.cs +++ b/server/Migrations/PodnomsDbContextModelSnapshot.cs @@ -5,9 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using PodNoms.Api.Models; using PodNoms.Api.Persistence; namespace PodNoms.Api.Migrations @@ -19,9 +16,117 @@ namespace PodNoms.Api.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.0-preview1-28290") + .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => { b.Property("Id") @@ -47,6 +152,8 @@ namespace PodNoms.Api.Migrations b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("AppUserId"); + b.Property("CreateDate") .ValueGeneratedOnAdd() .HasDefaultValueSql("getdate()"); @@ -68,6 +175,8 @@ namespace PodNoms.Api.Migrations b.HasKey("Id"); + b.HasIndex("AppUserId"); + b.HasIndex("UserId"); b.ToTable("Podcasts"); @@ -161,7 +270,111 @@ namespace PodNoms.Api.Migrations .IsUnique() .HasFilter("[Slug] IS NOT NULL"); - b.ToTable("Users"); + b.ToTable("UserDetails"); + }); + + modelBuilder.Entity("PodNoms.Api.Services.Auth.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("FacebookId"); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("PictureUrl"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("PodNoms.Api.Models.Playlist", b => @@ -174,6 +387,10 @@ namespace PodNoms.Api.Migrations modelBuilder.Entity("PodNoms.Api.Models.Podcast", b => { + b.HasOne("PodNoms.Api.Services.Auth.ApplicationUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId"); + b.HasOne("PodNoms.Api.Models.User", "User") .WithMany() .HasForeignKey("UserId"); diff --git a/server/Models/AppSettings.cs b/server/Models/AppSettings.cs index 35d73e3..1ddb329 100644 --- a/server/Models/AppSettings.cs +++ b/server/Models/AppSettings.cs @@ -1,13 +1,10 @@ -namespace PodNoms.Api.Models -{ - public class AppSettings - { +namespace PodNoms.Api.Models { + public class AppSettings { public string Version { get; set; } public string RssUrl { get; set; } } - public class StorageSettings - { + public class StorageSettings { public string ConnectionString { get; set; } public string CdnUrl { get; set; } diff --git a/server/Models/JwtIssuerOptions.cs b/server/Models/JwtIssuerOptions.cs new file mode 100644 index 0000000..3304811 --- /dev/null +++ b/server/Models/JwtIssuerOptions.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace PodNoms.Api.Models { + public class JwtIssuerOptions { + /// + /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. + /// + public string Issuer { get; set; } + + /// + /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. + /// + public string Subject { get; set; } + + /// + /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. + /// + public string Audience { get; set; } + + /// + /// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. + /// + public DateTime Expiration => IssuedAt.Add(ValidFor); + + /// + /// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. + /// + public DateTime NotBefore => DateTime.UtcNow; + + /// + /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. + /// + public DateTime IssuedAt => DateTime.UtcNow; + + /// + /// Set the timespan the token will be valid for (default is 120 min) + /// + public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); + + + + /// + /// "jti" (JWT ID) Claim (default ID is a GUID) + /// + public Func> JtiGenerator => + () => Task.FromResult(Guid.NewGuid().ToString()); + + /// + /// The signing key to use when generating tokens. + /// + public SigningCredentials SigningCredentials { get; set; } + } + +} \ No newline at end of file diff --git a/server/Models/Podcast.cs b/server/Models/Podcast.cs index 7262bff..855e7a3 100644 --- a/server/Models/Podcast.cs +++ b/server/Models/Podcast.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.Extensions.Options; +using PodNoms.Api.Services.Auth; namespace PodNoms.Api.Models { public class Podcast : BaseModel { public int Id { get; set; } public string Uid { get; set; } public User User { get; set; } + public ApplicationUser AppUser { get; set; } public string Title { get; set; } public string Description { get; set; } public string Slug { get; set; } diff --git a/server/Models/ViewModels/CredentialsViewModel.cs b/server/Models/ViewModels/CredentialsViewModel.cs new file mode 100644 index 0000000..e56965e --- /dev/null +++ b/server/Models/ViewModels/CredentialsViewModel.cs @@ -0,0 +1,8 @@ +using FluentValidation.Attributes; +namespace PodNoms.Api.Models.ViewModels { + [Validator(typeof(CredentialsViewModelValidator))] + public class CredentialsViewModel { + public string UserName { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/server/Models/ViewModels/CredentialsViewModelValidator.cs b/server/Models/ViewModels/CredentialsViewModelValidator.cs new file mode 100644 index 0000000..960e64a --- /dev/null +++ b/server/Models/ViewModels/CredentialsViewModelValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace PodNoms.Api.Models.ViewModels { + public class CredentialsViewModelValidator : AbstractValidator { + public CredentialsViewModelValidator() { + RuleFor(vm => vm.UserName).NotEmpty().WithMessage("Username cannot be empty"); + RuleFor(vm => vm.Password).NotEmpty().WithMessage("Password cannot be empty"); + RuleFor(vm => vm.Password).Length(6, 12).WithMessage("Password must be between 6 and 12 characters"); + } + } +} \ No newline at end of file diff --git a/server/Models/ViewModels/RegistrationViewModel.cs b/server/Models/ViewModels/RegistrationViewModel.cs new file mode 100644 index 0000000..7a856b6 --- /dev/null +++ b/server/Models/ViewModels/RegistrationViewModel.cs @@ -0,0 +1,10 @@ +namespace PodNoms.Api.Models.ViewModels { + public class RegistrationViewModel { + public string Email { get; set; } + 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 430fb1a..8688ce9 100644 --- a/server/Persistence/IUserRepository.cs +++ b/server/Persistence/IUserRepository.cs @@ -3,12 +3,11 @@ using PodNoms.Api.Models; namespace PodNoms.Api.Persistence { public interface IUserRepository { - User Get(int id); - User Get(string email); - Task GetAsync(string email); + User Get(string id); + Task GetAsync(string id); Task GetBySlugAsync(string slug); User UpdateRegistration(string email, string name, string sid, string providerId, string profileImage, string refreshToken); - string UpdateApiKey(User email); + string UpdateApiKey(User user); User AddOrUpdate(User user); } } \ No newline at end of file diff --git a/server/Persistence/PodcastRepository.cs b/server/Persistence/PodcastRepository.cs index fd19d00..0d5b738 100644 --- a/server/Persistence/PodcastRepository.cs +++ b/server/Persistence/PodcastRepository.cs @@ -37,10 +37,11 @@ namespace PodNoms.Api.Persistence { return ret; } - public async Task> GetAllAsync(string emailAddress) { + public async Task> GetAllAsync(string userId) { var ret = _context.Podcasts - .Where(u => u.User.EmailAddress == emailAddress) + .Where(u => u.AppUser.Id == userId) .Include(p => p.User) + .Include(p => p.AppUser) .OrderByDescending(p => p.Id); return await ret.ToListAsync(); } diff --git a/server/Persistence/PodnomsContext.cs b/server/Persistence/PodnomsContext.cs index 88a0dae..c8d7df7 100644 --- a/server/Persistence/PodnomsContext.cs +++ b/server/Persistence/PodnomsContext.cs @@ -1,14 +1,16 @@ using System; using System.IO; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; using PodNoms.Api.Models; +using PodNoms.Api.Services.Auth; namespace PodNoms.Api.Persistence { - public class PodnomsDbContext : DbContext { + public class PodnomsDbContext : IdentityDbContext { public PodnomsDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -36,6 +38,6 @@ namespace PodNoms.Api.Persistence { public DbSet Podcasts { get; set; } public DbSet PodcastEntries { get; set; } public DbSet Playlists { get; set; } - public DbSet Users { get; set; } + public DbSet UserDetails { get; set; } } } \ No newline at end of file diff --git a/server/Persistence/UserRepository.cs b/server/Persistence/UserRepository.cs index 310b456..b6f0bf1 100644 --- a/server/Persistence/UserRepository.cs +++ b/server/Persistence/UserRepository.cs @@ -15,19 +15,19 @@ namespace PodNoms.Api.Persistence { } public User Get(int id) { - return _context.Users.FirstOrDefault(u => u.Id == id); + return _context.UserDetails.FirstOrDefault(u => u.Id == id); } public User Get(string email) { - return _context.Users.FirstOrDefault(u => u.EmailAddress == email); + return _context.UserDetails.FirstOrDefault(u => u.EmailAddress == email); } public async Task GetAsync(string email) { - return await _context.Users + return await _context.UserDetails .Where(u => u.EmailAddress == email) .FirstOrDefaultAsync(); } public async Task GetBySlugAsync(string slug) { - var user = await _context.Users + var user = await _context.UserDetails .Where(u => u.Slug == slug) .FirstOrDefaultAsync(); @@ -36,9 +36,9 @@ namespace PodNoms.Api.Persistence { public User AddOrUpdate(User user) { if (user.Id != 0) { - _context.Users.Attach(user); + _context.UserDetails.Attach(user); } else { - _context.Users.Add(user); + _context.UserDetails.Add(user); } return user; @@ -46,7 +46,7 @@ namespace PodNoms.Api.Persistence { public User UpdateRegistration(string email, string name, string sid, string providerId, string profileImage, string refreshToken) { - var user = _context.Users.FirstOrDefault(u => u.EmailAddress == email); + var user = _context.UserDetails.FirstOrDefault(u => u.EmailAddress == email); if (user == null) { user = new User(); @@ -57,7 +57,7 @@ namespace PodNoms.Api.Persistence { var c = user.FullName ?? email?.Split('@')[0] ?? string.Empty; if (!string.IsNullOrEmpty(c)) { user.Slug = c.Slugify( - from u in _context.Users select u.Slug); + from u in _context.UserDetails select u.Slug); } } if (string.IsNullOrEmpty(user.Uid)) { @@ -81,7 +81,7 @@ namespace PodNoms.Api.Persistence { if (user != null) { do { newKey = Randomisers.RandomString(16); - } while (_context.Users.FirstOrDefault(u => u.ApiKey == newKey) != null); + } while (_context.UserDetails.FirstOrDefault(u => u.ApiKey == newKey) != null); } user.ApiKey = newKey; return newKey; diff --git a/server/PodNoms.Api.csproj b/server/PodNoms.Api.csproj index 775b361..24b8854 100644 --- a/server/PodNoms.Api.csproj +++ b/server/PodNoms.Api.csproj @@ -11,6 +11,7 @@ + diff --git a/server/Program.cs b/server/Program.cs index 9ad31e2..bea9326 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -9,23 +9,19 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -namespace PodNoms.Api -{ - public class Program - { +namespace PodNoms.Api { + public class Program { static bool isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == EnvironmentName.Development; - public static void Main(string[] args) - { + public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup() .UseUrls("http://0.0.0.0:5000") - .UseKestrel(options => - { + .UseKestrel(options => { options.Limits.MaxRequestBodySize = 1073741824; //1Gb // if (isDevelopment) // { diff --git a/server/Providers/MappingProvider.cs b/server/Providers/MappingProvider.cs index 48800c9..c65a155 100644 --- a/server/Providers/MappingProvider.cs +++ b/server/Providers/MappingProvider.cs @@ -3,15 +3,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using PodNoms.Api.Models; using PodNoms.Api.Models.ViewModels; +using PodNoms.Api.Services.Auth; -namespace PodNoms.Api.Providers -{ - public class MappingProvider : Profile - { +namespace PodNoms.Api.Providers { + public class MappingProvider : Profile { private readonly IConfiguration _options; public MappingProvider() { } - public MappingProvider(IConfiguration options) - { + public MappingProvider(IConfiguration options) { this._options = options; //Domain to API Resource @@ -29,7 +27,7 @@ namespace PodNoms.Api.Providers .ForMember( src => src.AudioUrl, e => e.MapFrom(m => $"{this._options.GetSection("Storage")["CdnUrl"]}{m.AudioUrl}")); - + CreateMap() .ForMember( src => src.Name, @@ -37,13 +35,17 @@ namespace PodNoms.Api.Providers //API Resource to Domain CreateMap() - .ForMember(v => v.ImageUrl, opt => opt.Ignore()) + .ForMember(v => v.ImageUrl, map => map.Ignore()) ; CreateMap() .ForMember( e => e.ImageUrl, - opt => opt.MapFrom(m => m.ImageUrl)) + map => map.MapFrom(vm => vm.ImageUrl)) ; + CreateMap() + .ForMember( + e => e.UserName, + map => map.MapFrom(vm => vm.Email)); } } } \ No newline at end of file diff --git a/server/Services/Auth/ApplicationUser.cs b/server/Services/Auth/ApplicationUser.cs new file mode 100644 index 0000000..8dd848f --- /dev/null +++ b/server/Services/Auth/ApplicationUser.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Identity; + +namespace PodNoms.Api.Services.Auth { + public class ApplicationUser : IdentityUser { + // Extended Properties + public string FirstName { get; set; } + public string LastName { get; set; } + public long? FacebookId { get; set; } + public string PictureUrl { get; set; } + } +} \ No newline at end of file diff --git a/server/Services/Auth/Constants.cs b/server/Services/Auth/Constants.cs new file mode 100644 index 0000000..96e8e2d --- /dev/null +++ b/server/Services/Auth/Constants.cs @@ -0,0 +1,13 @@ +namespace PodNoms.Api.Services.Auth { + public static class Constants { + public static class Strings { + public static class JwtClaimIdentifiers { + public const string Rol = "rol", Id = "id"; + } + + public static class JwtClaims { + public const string ApiAccess = "api_access"; + } + } + } +} diff --git a/server/Services/Auth/IJwtFactory.cs b/server/Services/Auth/IJwtFactory.cs new file mode 100644 index 0000000..bfceba4 --- /dev/null +++ b/server/Services/Auth/IJwtFactory.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; +using System.Threading.Tasks; + +namespace PodNoms.Api.Services.Auth { + public interface IJwtFactory { + Task GenerateEncodedToken(string userName, ClaimsIdentity identity); + ClaimsIdentity GenerateClaimsIdentity(string userName, string id); + } +} \ No newline at end of file diff --git a/server/Services/Auth/JwtFactory.cs b/server/Services/Auth/JwtFactory.cs new file mode 100644 index 0000000..3d77008 --- /dev/null +++ b/server/Services/Auth/JwtFactory.cs @@ -0,0 +1,73 @@ + +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using PodNoms.Api.Models; + +namespace PodNoms.Api.Services.Auth { + public class JwtFactory : IJwtFactory { + private readonly JwtIssuerOptions _jwtOptions; + + public JwtFactory(IOptions jwtOptions) { + _jwtOptions = jwtOptions.Value; + ThrowIfInvalidOptions(_jwtOptions); + } + + public async Task GenerateEncodedToken(string userName, ClaimsIdentity identity) { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userName), + new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), + new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64), + identity.FindFirst(Constants.Strings.JwtClaimIdentifiers.Rol), + identity.FindFirst(Constants.Strings.JwtClaimIdentifiers.Id) + }; + + // Create the JWT security token and encode it. + var jwt = new JwtSecurityToken( + issuer: _jwtOptions.Issuer, + audience: _jwtOptions.Audience, + claims: claims, + notBefore: _jwtOptions.NotBefore, + expires: _jwtOptions.Expiration, + signingCredentials: _jwtOptions.SigningCredentials); + + var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); + + return encodedJwt; + } + + public ClaimsIdentity GenerateClaimsIdentity(string userName, string id) { + return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[] + { + new Claim(Constants.Strings.JwtClaimIdentifiers.Id, id), + new Claim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess) + }); + } + + /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - + new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) + .TotalSeconds); + + private static void ThrowIfInvalidOptions(JwtIssuerOptions options) { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.ValidFor <= TimeSpan.Zero) { + throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); + } + + if (options.SigningCredentials == null) { + throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); + } + + if (options.JtiGenerator == null) { + throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); + } + } + } +} \ No newline at end of file diff --git a/server/Services/Auth/Tokens.cs b/server/Services/Auth/Tokens.cs new file mode 100644 index 0000000..3a1f4a4 --- /dev/null +++ b/server/Services/Auth/Tokens.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PodNoms.Api.Models; + +namespace PodNoms.Api.Services.Auth { + public class Tokens { + public static async Task GenerateJwt(ClaimsIdentity identity, IJwtFactory jwtFactory, string userName, + JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings) { + var response = new { + id = identity.Claims.Single(c => c.Type == "id").Value, + auth_token = await jwtFactory.GenerateEncodedToken(userName, identity), + expires_in = (int)jwtOptions.ValidFor.TotalSeconds + }; + + return JsonConvert.SerializeObject(response, serializerSettings); + } + } +} \ No newline at end of file diff --git a/server/Startup.cs b/server/Startup.cs index 5fbefc3..6a5bfda 100644 --- a/server/Startup.cs +++ b/server/Startup.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; +using FluentValidation.AspNetCore; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; @@ -41,9 +42,14 @@ using PodNoms.Api.Services.Push.Extensions; using Swashbuckle.AspNetCore.Swagger; using PodNoms.Api.Services.Push.Formatters; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace PodNoms.Api { public class Startup { + private const string SecretKey = "QGfaEMNASkNMGLKA3LjgPdkPfFEy3n40"; // todo: get this from somewhere secure + private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)); public IConfiguration Configuration { get; } public Startup(IConfiguration configuration) { @@ -105,39 +111,65 @@ namespace PodNoms.Api { }); services.AddHttpClient(); + var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions)); + // Configure JwtIssuerOptions + services.Configure(options => { + //TODO: Remove this in production, only for testing + options.ValidFor = TimeSpan.FromDays(28); + options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; + options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; + options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256); + }); + var tokenValidationParameters = new TokenValidationParameters { + ValidateIssuer = true, + ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], + + ValidateAudience = true, + ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], + + ValidateIssuerSigningKey = true, + IssuerSigningKey = _signingKey, + + RequireExpirationTime = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(options => { - options.Audience = Configuration["auth0:clientId"]; - options.Authority = $"https://{Configuration["auth0:domain"]}/"; - options.TokenValidationParameters = new TokenValidationParameters { - NameClaimType = "name" + }).AddJwtBearer(configureOptions => { + configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; + configureOptions.TokenValidationParameters = tokenValidationParameters; + configureOptions.SaveToken = true; + configureOptions.Events = new JwtBearerEvents() { + //Don't need this now we've removed Auth0 + // OnTokenValidated = AuthenticationMiddleware.OnTokenValidated }; - options.Events = new JwtBearerEvents() { - OnTokenValidated = AuthenticationMiddleware.OnTokenValidated - }; - options.Events.OnMessageReceived = context => { + configureOptions.Events.OnMessageReceived = context => { StringValues token; if (context.Request.Path.Value.StartsWith("/hubs/") && context.Request.Query.TryGetValue("token", out token)) { context.Token = token; } - return Task.CompletedTask; - }; + }; }); - var defaultPolicy = - new AuthorizationPolicyBuilder() - .AddAuthenticationSchemes("Bearer") - .RequireAuthenticatedUser() - .Build(); - services.AddAuthorization(j => { - j.DefaultPolicy = defaultPolicy; + j.AddPolicy("ApiUser", policy => policy.RequireClaim( + Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess)); }); + // add identity + var identityBuilder = services.AddIdentityCore(o => { + // configure identity options + o.Password.RequireDigit = false; + o.Password.RequireLowercase = false; + o.Password.RequireUppercase = false; + o.Password.RequireNonAlphanumeric = false; + o.Password.RequiredLength = 6; + }); + identityBuilder = new IdentityBuilder(identityBuilder.UserType, typeof(IdentityRole), identityBuilder.Services); + identityBuilder.AddEntityFrameworkStores().AddDefaultTokenProviders(); services.AddMvc(options => { options.OutputFormatters.Add(new XmlSerializerOutputFormatter()); @@ -150,7 +182,8 @@ namespace PodNoms.Api { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; }) - .AddXmlSerializerFormatters(); + .AddXmlSerializerFormatters() + .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining()); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Title = "Podnoms.API", Version = "v1" }); @@ -174,6 +207,8 @@ namespace PodNoms.Api { services.AddTransient(); services.AddTransient(); + services.TryAddTransient(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/server/Utils/DateUtils.cs b/server/Utils/DateUtils.cs index 250a9f7..7b7589c 100644 --- a/server/Utils/DateUtils.cs +++ b/server/Utils/DateUtils.cs @@ -1,18 +1,13 @@ using System; -namespace PodNoms.Api.Utils -{ - - public class DateUtils - { - public static DateTime ConvertFromUnixTimestamp(double timestamp) - { +namespace PodNoms.Api.Utils { + public class DateUtils { + public static DateTime ConvertFromUnixTimestamp(double timestamp) { DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return origin.AddSeconds(timestamp); } - public static double ConvertToUnixTimestamp(DateTime date) - { + public static double ConvertToUnixTimestamp(DateTime date) { DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); TimeSpan diff = date.ToUniversalTime() - origin; return Math.Floor(diff.TotalSeconds); diff --git a/server/Utils/Errors.cs b/server/Utils/Errors.cs new file mode 100644 index 0000000..0fd6713 --- /dev/null +++ b/server/Utils/Errors.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace PodNoms.Api.Utils { + public static class Errors { + public static ModelStateDictionary AddErrorsToModelState(IdentityResult identityResult, ModelStateDictionary modelState) { + foreach (var e in identityResult.Errors) { + modelState.TryAddModelError(e.Code, e.Description); + } + + return modelState; + } + + public static ModelStateDictionary AddErrorToModelState(string code, string description, ModelStateDictionary modelState) { + modelState.TryAddModelError(code, description); + return modelState; + } + } +} \ No newline at end of file