diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6443a6a..3e5a4aa 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,6 +1,7 @@ import {NgModule} from "@angular/core"; import {RouterModule, Routes} from "@angular/router"; import {GameDataResolver} from "./core/resolvers/game-data.resolver"; +import {UserResolver} from "./core/resolvers/user.resolver"; const rootRoutes: Routes = [ { @@ -10,6 +11,13 @@ const rootRoutes: Routes = [ gameData: GameDataResolver } }, + { + path: 'oauth', + loadChildren: () => import('./pages/oauth/oauth.module').then(m => m.OauthModule), + resolve: { + user: UserResolver + } + }, { path: 'embed', loadChildren: () => import('./pages/embedded/embedded.module').then(m => m.EmbeddedModule), @@ -29,6 +37,7 @@ const rootRoutes: Routes = [ redirectTo: 'browser', pathMatch: 'full' }, + {path: '**', redirectTo: 'browser'} ]; @NgModule({ @@ -41,4 +50,5 @@ const rootRoutes: Routes = [ RouterModule ] }) -export class AppRoutingModule {} +export class AppRoutingModule { +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 589a44d..dcc221a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -14,11 +14,12 @@ import {ReactiveFormsModule} from "@angular/forms"; BrowserModule, HttpClientModule, AppRoutingModule, - ReactiveFormsModule + ReactiveFormsModule, ], providers: [ communicationLayerServiceProvider ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { +} diff --git a/src/app/core/resolvers/user.resolver.spec.ts b/src/app/core/resolvers/user.resolver.spec.ts new file mode 100644 index 0000000..d6d6fe6 --- /dev/null +++ b/src/app/core/resolvers/user.resolver.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserResolver } from './user.resolver'; + +describe('UserResolver', () => { + let resolver: UserResolver; + + beforeEach(() => { + TestBed.configureTestingModule({}); + resolver = TestBed.inject(UserResolver); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); +}); diff --git a/src/app/core/resolvers/user.resolver.ts b/src/app/core/resolvers/user.resolver.ts new file mode 100644 index 0000000..a7bd4d3 --- /dev/null +++ b/src/app/core/resolvers/user.resolver.ts @@ -0,0 +1,18 @@ +import {Injectable} from '@angular/core'; +import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; +import {Observable} from 'rxjs'; +import {User} from "../../modules/api/interfaces/user"; +import {UserService} from "../../modules/user/services/user.service"; +import {take} from "rxjs/operators"; + +@Injectable({ + providedIn: 'root' +}) +export class UserResolver implements Resolve { + public constructor(private readonly userService: UserService) { + } + + public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.userService.signedInUser$.pipe(take(1)); + } +} diff --git a/src/app/modules/api/interfaces/oauth-client.ts b/src/app/modules/api/interfaces/oauth-client.ts new file mode 100644 index 0000000..3cec718 --- /dev/null +++ b/src/app/modules/api/interfaces/oauth-client.ts @@ -0,0 +1,6 @@ +export interface OauthClient { + oauthProvider: string; + providerUserId: string; + providerUsername: string; + providerUserDiscriminator?: string; +} diff --git a/src/app/modules/api/interfaces/user.ts b/src/app/modules/api/interfaces/user.ts index 060eddb..3fd92df 100644 --- a/src/app/modules/api/interfaces/user.ts +++ b/src/app/modules/api/interfaces/user.ts @@ -1,6 +1,9 @@ import {UserShort} from "./user-short"; +import {OauthClient} from "./oauth-client"; export interface User extends UserShort { uniqueToken: string, - email: string + email: string, + isOAuthUser: boolean, + oauthClients: OauthClient[] } diff --git a/src/app/modules/api/services/api.service.ts b/src/app/modules/api/services/api.service.ts index b59e39b..00ea800 100644 --- a/src/app/modules/api/services/api.service.ts +++ b/src/app/modules/api/services/api.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {RotationCreate} from "../interfaces/rotation-create"; import {RotationUpdate} from "../interfaces/rotation-update"; import {Rotation} from "../interfaces/rotation"; @@ -9,7 +9,6 @@ import {User} from "../interfaces/user"; import {environment} from "../../../../environments/environment"; import {HttpClient} from "@angular/common/http"; import {Observable} from "rxjs"; -import {map} from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -17,15 +16,16 @@ import {map} from "rxjs/operators"; export class ApiService { private readonly API_BASE_URL = environment.apiBaseUrl; - public constructor(private readonly httpClient: HttpClient) {} + public constructor(private readonly httpClient: HttpClient) { + } // AUTH signIn(email: string, password: string): Observable { - return this.request('/auth/login', 'POST', JSON.stringify({ email, password })) as Observable; + return this.request('/auth/login', 'POST', JSON.stringify({email, password})) as Observable; } signUp(email: string, username: string, password: string) { - return this.request('/auth/signup', 'POST', JSON.stringify({ email, password, username })); + return this.request('/auth/signup', 'POST', JSON.stringify({email, password, username})); } me(): Observable { @@ -80,7 +80,7 @@ export class ApiService { const paramString = paramArray.length ? `?${paramArray.join('&')}` : ''; - return this.request(`/rotation/${ paramString }`, 'GET') as Observable>; + return this.request(`/rotation/${paramString}`, 'GET') as Observable>; } // USER @@ -92,6 +92,14 @@ export class ApiService { return this.request('/user/rotations', 'GET') as Observable>; } + usernameTaken(username: string): Observable { + return this.request(`/user/name-taken/${username}`, 'GET') as Observable; + } + + changeUsername(username: string): Observable { + return this.request(`/user/name`, 'POST', {username}) as Observable; + } + // Token userTokenFavourites(token: string): Observable> { return this.request(`/token/${token}/favourites`, 'GET') as Observable>; @@ -101,17 +109,17 @@ export class ApiService { return this.request(`/token/${token}/rotations`, 'GET') as Observable>; } - private request(url: string, method: 'POST' | 'PATCH' | 'GET' | 'DELETE', body?: string) { + private request(url: string, method: 'POST' | 'PATCH' | 'GET' | 'DELETE', body?: string | Record) { return this.httpClient.request( - method, - `${this.API_BASE_URL}${url}`, - { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true, - body - } + method, + `${this.API_BASE_URL}${url}`, + { + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true, + body + } ); } } diff --git a/src/app/modules/toolbar/toolbar.component.html b/src/app/modules/toolbar/toolbar.component.html index 8d4586f..a6c2ae1 100644 --- a/src/app/modules/toolbar/toolbar.component.html +++ b/src/app/modules/toolbar/toolbar.component.html @@ -1,7 +1,7 @@ + [classJobs]="classJobs$ | async" + [currentClassJob]="currentClassJob$ | async" + (selectClassJob)="selectClassJob($event)"> Rotation Hero @@ -19,9 +19,9 @@ + [user]="(user$ | async)!"> diff --git a/src/app/modules/toolbar/toolbar.component.ts b/src/app/modules/toolbar/toolbar.component.ts index e80fc81..6b387d2 100644 --- a/src/app/modules/toolbar/toolbar.component.ts +++ b/src/app/modules/toolbar/toolbar.component.ts @@ -35,11 +35,12 @@ export class ToolbarComponent { public readonly classJobs$ = this.gameDataService.classJobs$; public readonly currentClassJob$ = this.appStateService.currentClassJob$ public readonly user$ = this.userService.signedInUser$; + public readonly displayedUserName$ = this.userService.displayedUserName$; constructor( - public readonly gameDataService: GameDataService, - private readonly appStateService: AppStateService, - private readonly userService: UserService + public readonly gameDataService: GameDataService, + private readonly appStateService: AppStateService, + private readonly userService: UserService ) { } diff --git a/src/app/modules/user/components/profile/profile.component.html b/src/app/modules/user/components/profile/profile.component.html index 604c98a..c397819 100644 --- a/src/app/modules/user/components/profile/profile.component.html +++ b/src/app/modules/user/components/profile/profile.component.html @@ -1,13 +1,13 @@

- ID:
+ ID:
{{ user?.id }}

- Username:
+ Username:
{{ user?.username }}

-

- ACT Overlay URL:
+

+ ACT Overlay URL:
{{ user?.uniqueToken }}

diff --git a/src/app/modules/user/components/profile/profile.component.ts b/src/app/modules/user/components/profile/profile.component.ts index e32c92c..9eb6e03 100644 --- a/src/app/modules/user/components/profile/profile.component.ts +++ b/src/app/modules/user/components/profile/profile.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, ChangeDetectionStrategy, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {UserService} from "../../services/user.service"; import {User} from "../../../api/interfaces/user"; @@ -10,9 +10,10 @@ import {User} from "../../../api/interfaces/user"; }) export class ProfileComponent { - @Input() user: User | null = null; + @Input() user!: User; - constructor(private readonly userService: UserService) { } + constructor(private readonly userService: UserService) { + } logout() { this.userService.signOutSubject$.next(); diff --git a/src/app/modules/user/components/sign-in/sign-in.component.html b/src/app/modules/user/components/sign-in/sign-in.component.html index ae9d870..a41ef45 100644 --- a/src/app/modules/user/components/sign-in/sign-in.component.html +++ b/src/app/modules/user/components/sign-in/sign-in.component.html @@ -5,13 +5,28 @@ (ngSubmit)="onSubmit(signInFormGroup.value)"> + [disabled]="!signInFormGroup.valid">Sign in + + + + + + + diff --git a/src/app/modules/user/components/sign-in/sign-in.component.scss b/src/app/modules/user/components/sign-in/sign-in.component.scss index 838dce3..d21878f 100644 --- a/src/app/modules/user/components/sign-in/sign-in.component.scss +++ b/src/app/modules/user/components/sign-in/sign-in.component.scss @@ -1,4 +1,6 @@ -:host { display: block; } +:host { + display: block; +} .rh-sign-in { &__title { @@ -22,10 +24,10 @@ &__input { display: block; color: white; - background: rgba(255,255,255,0.1); + background: rgba(255, 255, 255, 0.1); border: 0; outline: 0; - border-bottom: 2px solid rgba(255,255,255,0.2); + border-bottom: 2px solid rgba(255, 255, 255, 0.2); line-height: 1.5; width: 100%; padding: 6px 8px; @@ -48,4 +50,57 @@ border-color: gray; } } + + &__separator { + background: linear-gradient(to right, transparent, white, transparent) no-repeat center center; + background-size: 100% 1px; + color: white; + text-align: center; + margin: 8px 0; + + span { + display: inline-block; + background: #323232; + font-size: 12px; + padding: 0 6px; + } + } + + &__social-button { + cursor: pointer; + font-size: inherit; + width: 100%; + border: 0; + padding: 0; + text-align: left; + display: flex; + align-items: stretch; + + &::before { + content: ''; + display: block; + width: 40px; + } + + &--discord { + background: #8c9eff; + color: white; + + &::before { + background: white url("/assets/images/social/discord.png") no-repeat center center; + background-size: 75%; + } + } + } + + &__social-button-label { + padding: 8px 12px; + font-size: 16px; + font-weight: 700; + } + + &__social-auth-label { + color: white; + font-size: 12px; + } } diff --git a/src/app/modules/user/components/sign-in/sign-in.component.ts b/src/app/modules/user/components/sign-in/sign-in.component.ts index 1a321e1..415d8bb 100644 --- a/src/app/modules/user/components/sign-in/sign-in.component.ts +++ b/src/app/modules/user/components/sign-in/sign-in.component.ts @@ -1,6 +1,13 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {FormControl, FormGroup, Validators} from "@angular/forms"; import {UserService} from "../../services/user.service"; +import {environment} from "../../../../../environments/environment"; + +interface OAuthProvider { + name: string, + url: string, + className: string +} @Component({ selector: 'rh-sign-in', @@ -10,14 +17,27 @@ import {UserService} from "../../services/user.service"; }) export class SignInComponent { - public signInFormGroup = new FormGroup({ - email: new FormControl('', [ Validators.required, Validators.email ]), - password: new FormControl('', [ Validators.required ]) + public readonly oauthProviders: OAuthProvider[] = [ + { + name: 'Discord', + className: 'discord', + url: environment.apiBaseUrl + '/auth/provider/discord' + } + ]; + + public readonly signInFormGroup = new FormGroup({ + email: new FormControl('', [Validators.required, Validators.email]), + password: new FormControl('', [Validators.required]) }); - public constructor(private readonly userService: UserService) {} + public constructor(private readonly userService: UserService) { + } - public onSubmit(credentials: { email: string, password: string}) { + public onSubmit(credentials: { email: string, password: string }) { this.userService.signInSubject$.next(credentials); } + + public signInWithProvider(provider: OAuthProvider): void { + window.location.href = provider.url; + } } diff --git a/src/app/modules/user/services/user.service.ts b/src/app/modules/user/services/user.service.ts index 60d8db5..bffbbd5 100644 --- a/src/app/modules/user/services/user.service.ts +++ b/src/app/modules/user/services/user.service.ts @@ -1,31 +1,67 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {ApiService} from "../../api/services/api.service"; -import {of, Subject} from "rxjs"; -import {catchError, shareReplay, startWith, switchMap, switchMapTo, tap} from "rxjs/operators"; +import {Observable, of, ReplaySubject, Subject} from "rxjs"; +import {catchError, map, share, switchMap, take} from "rxjs/operators"; +import {User} from "../../api/interfaces/user"; +import {Router} from "@angular/router"; @Injectable({ providedIn: 'root' }) export class UserService { + public readonly signedInUser$: ReplaySubject = new ReplaySubject(1); public readonly signInSubject$: Subject<{ email: string, password: string }> = new Subject(); public readonly signOutSubject$: Subject = new Subject(); - public readonly signInRequests$ = this.signInSubject$.pipe( - switchMap(({ email, password}) => - this.api.signIn(email, password) - .pipe(catchError(() => of(null))) - ), - startWith(null) - ); + public readonly displayedUserName$: Observable = this.signedInUser$ + .pipe( + map((user) => { + if (!user) { + return ''; + } + + return user.username; + }) + ); - public readonly signedInUser$ = this.signOutSubject$.pipe( - startWith(null), - switchMapTo(this.signInRequests$), - shareReplay(1) + private readonly signInRequests$ = this.signInSubject$.pipe( + switchMap(({email, password}) => + this.api.signIn(email, password) + .pipe(catchError(() => of(null))) + ) ); - public constructor(private readonly api: ApiService) { + private readonly signOutRequests$ = this.signOutSubject$.pipe( + switchMap(() => + this.api.logout() + .pipe(catchError(() => of(null))) + ), + share() + ) + + public constructor(private readonly api: ApiService, + private readonly router: Router) { this.signedInUser$.subscribe(); + this.signInRequests$.subscribe((user) => this.signedInUser$.next(user)); + this.signOutRequests$.subscribe(() => this.signedInUser$.next(null)); + + this.loadCurrentUser(); + } + + public loadCurrentUser() { + this.api.me().subscribe((user) => this.signedInUser$.next(user)); + } + + public logout() { + this.signOutRequests$ + .pipe( + take(1) + ) + .subscribe( + () => this.router.navigate(['/browser']) + ); + + this.signOutSubject$.next(); } } diff --git a/src/app/modules/user/user.component.html b/src/app/modules/user/user.component.html deleted file mode 100644 index 8e3b85b..0000000 --- a/src/app/modules/user/user.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/app/modules/user/user.component.scss b/src/app/modules/user/user.component.scss deleted file mode 100644 index 9be3c5b..0000000 --- a/src/app/modules/user/user.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -:host { - width: 300px; - z-index: 100; - padding: 16px; - background: rgba(36, 36, 36, 0.6); -} diff --git a/src/app/modules/user/user.component.ts b/src/app/modules/user/user.component.ts deleted file mode 100644 index 6539d13..0000000 --- a/src/app/modules/user/user.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import {ActiveUserView} from "./enums/active-user-view"; -import {UserService} from "./services/user.service"; - -@Component({ - selector: 'rh-user', - templateUrl: './user.component.html', - styleUrls: ['./user.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class UserComponent { - - public user$ = this.userService.signedInUser$; - - public activeView: ActiveUserView = ActiveUserView.SignIn; - - public readonly ActiveUserView = ActiveUserView; - - constructor(private readonly userService: UserService) { } - -} diff --git a/src/app/modules/user/user.module.ts b/src/app/modules/user/user.module.ts index 01adc7a..ba1c41f 100644 --- a/src/app/modules/user/user.module.ts +++ b/src/app/modules/user/user.module.ts @@ -1,26 +1,21 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { SignInComponent } from './components/sign-in/sign-in.component'; -import { SignUpComponent } from './components/sign-up/sign-up.component'; -import { ProfileComponent } from './components/profile/profile.component'; -import { UserComponent } from './user.component'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {SignInComponent} from './components/sign-in/sign-in.component'; +import {SignUpComponent} from './components/sign-up/sign-up.component'; +import {ProfileComponent} from './components/profile/profile.component'; import {ReactiveFormsModule} from "@angular/forms"; -import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component'; -import { UserOverlayComponent } from './components/user-overlay/user-overlay.component'; - - +import {ForgotPasswordComponent} from './components/forgot-password/forgot-password.component'; +import {UserOverlayComponent} from './components/user-overlay/user-overlay.component'; @NgModule({ declarations: [ SignInComponent, SignUpComponent, ProfileComponent, - UserComponent, ForgotPasswordComponent, UserOverlayComponent ], exports: [ - UserComponent, UserOverlayComponent, ProfileComponent ], @@ -29,4 +24,5 @@ import { UserOverlayComponent } from './components/user-overlay/user-overlay.com ReactiveFormsModule ] }) -export class UserModule { } +export class UserModule { +} diff --git a/src/app/pages/browser/browser-routing.module.ts b/src/app/pages/browser/browser-routing.module.ts index 86cae33..8b63f7c 100644 --- a/src/app/pages/browser/browser-routing.module.ts +++ b/src/app/pages/browser/browser-routing.module.ts @@ -1,20 +1,50 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import {RouterModule, Routes} from "@angular/router"; +import {Injectable, NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterModule, + RouterStateSnapshot, + Routes, + UrlTree +} from "@angular/router"; import {BrowserComponent} from "./browser.component"; +import {ApiService} from "../../modules/api/services/api.service"; +import {Observable} from "rxjs"; +import {map} from "rxjs/operators"; + +@Injectable() +class IsLoggedOutOrHasUsername implements CanActivate { + public constructor( + private readonly apiService: ApiService, + private readonly router: Router + ) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + return this.apiService.me() + .pipe( + map((user) => Boolean(!user || user.username !== null) ? true : this.router.createUrlTree(['/oauth'])) + ); + } +} const routes: Routes = [ { path: '', - component: BrowserComponent + component: BrowserComponent, + canActivate: [IsLoggedOutOrHasUsername] } ] @NgModule({ declarations: [], + providers: [IsLoggedOutOrHasUsername], imports: [ CommonModule, RouterModule.forChild(routes) ] }) -export class BrowserRoutingModule { } +export class BrowserRoutingModule { +} diff --git a/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.html b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.html new file mode 100644 index 0000000..eedd494 --- /dev/null +++ b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.html @@ -0,0 +1,14 @@ +
+ +
+
+
+ {{ user.oauthClients[0].providerUsername}} + + #{{user.oauthClients[0].providerUserDiscriminator}} + +
+
{{ user.oauthClients[0].oauthProvider }}
+
diff --git a/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.scss b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.scss new file mode 100644 index 0000000..8d4ed46 --- /dev/null +++ b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.scss @@ -0,0 +1,30 @@ +:host { + display: flex; + border-radius: 10px; + overflow: hidden; + + &.discord .rh-oauth-provider-user-badge__container { + background: #8c9eff; + color: white; + } +} + +.rh-oauth-provider-user-badge { + &__provider-icon { + background: white; + } + + &__user { + font-weight: 700; + } + + &__user-discriminator { + opacity: .7; + font-weight: 300; + } + + &__container { + padding: 6px; + flex-grow: 1; + } +} \ No newline at end of file diff --git a/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.spec.ts b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.spec.ts new file mode 100644 index 0000000..9b75b1c --- /dev/null +++ b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OauthProviderUserBadgeComponent } from './oauth-provider-user-badge.component'; + +describe('OauthProviderUserBadgeComponent', () => { + let component: OauthProviderUserBadgeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ OauthProviderUserBadgeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OauthProviderUserBadgeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.ts b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.ts new file mode 100644 index 0000000..91523b4 --- /dev/null +++ b/src/app/pages/oauth/oauth-provider-user-badge/oauth-provider-user-badge.component.ts @@ -0,0 +1,20 @@ +import {ChangeDetectionStrategy, Component, HostBinding, Input} from '@angular/core'; +import {User} from "../../../modules/api/interfaces/user"; + +@Component({ + selector: 'rh-oauth-provider-user-badge', + templateUrl: './oauth-provider-user-badge.component.html', + styleUrls: ['./oauth-provider-user-badge.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OauthProviderUserBadgeComponent { + + @Input() public user!: User; + + @HostBinding('class') + public provider: string = ''; + + public ngOnChanges() { + this.provider = this.user?.oauthClients[0].oauthProvider; + } +} diff --git a/src/app/pages/oauth/oauth-routing.module.ts b/src/app/pages/oauth/oauth-routing.module.ts new file mode 100644 index 0000000..d8cb171 --- /dev/null +++ b/src/app/pages/oauth/oauth-routing.module.ts @@ -0,0 +1,55 @@ +import {Injectable, NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterModule, + RouterStateSnapshot, + Routes, + UrlTree +} from "@angular/router"; +import {OauthComponent} from "./oauth.component"; +import {Observable} from "rxjs"; +import {map} from "rxjs/operators"; +import {ApiService} from "../../modules/api/services/api.service"; + +@Injectable() +class IsOauthUserWithoutName implements CanActivate { + public constructor( + private readonly apiService: ApiService, + private readonly router: Router + ) { + } + + public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + return this.apiService.me() + .pipe( + map((user) => Boolean(user && user.isOAuthUser && user.username === null) ? true : this.router.createUrlTree(['/browser'])) + ); + } +} + +const routes: Routes = [ + { + path: '', + component: OauthComponent, + canActivate: [IsOauthUserWithoutName] + } +]; + +@NgModule({ + declarations: [], + providers: [ + IsOauthUserWithoutName + ], + imports: [ + CommonModule, + RouterModule.forChild(routes) + ], + exports: [ + RouterModule + ] +}) +export class OauthRoutingModule { +} diff --git a/src/app/pages/oauth/oauth.component.html b/src/app/pages/oauth/oauth.component.html new file mode 100644 index 0000000..7cb6226 --- /dev/null +++ b/src/app/pages/oauth/oauth.component.html @@ -0,0 +1,41 @@ +
+ + +
+ + +
+

Choose a username

+ + + + This username is already taken :( + + + + +
+
+
diff --git a/src/app/pages/oauth/oauth.component.scss b/src/app/pages/oauth/oauth.component.scss new file mode 100644 index 0000000..a4244ab --- /dev/null +++ b/src/app/pages/oauth/oauth.component.scss @@ -0,0 +1,41 @@ +.rh-oauth { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #545561; + height: 100%; + + &__panel { + max-width: 400px; + padding: 16px; + border-radius: 16px; + background: #323232; + color: white; + font-size: 14px; + margin-bottom: 15%; + box-shadow: inset 0 2px 5px black; + border: 2px solid gray; + } + + &__logo { + height: 100px; + } + + &__username-input { + display: block; + color: #fff; + background: #ffffff1a; + border: 0; + outline: 0; + border-bottom: 2px solid #fff3; + line-height: 1.5; + width: 100%; + padding: 6px 8px; + box-sizing: border-box; + } +} + +button { + margin-top: 8px; +} diff --git a/src/app/modules/user/user.component.spec.ts b/src/app/pages/oauth/oauth.component.spec.ts similarity index 57% rename from src/app/modules/user/user.component.spec.ts rename to src/app/pages/oauth/oauth.component.spec.ts index e6bf596..ed653b4 100644 --- a/src/app/modules/user/user.component.spec.ts +++ b/src/app/pages/oauth/oauth.component.spec.ts @@ -1,20 +1,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserComponent } from './user.component'; +import { OauthComponent } from './oauth.component'; -describe('UserComponent', () => { - let component: UserComponent; - let fixture: ComponentFixture; +describe('OauthComponent', () => { + let component: OauthComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ UserComponent ] + declarations: [ OauthComponent ] }) .compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(UserComponent); + fixture = TestBed.createComponent(OauthComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/pages/oauth/oauth.component.ts b/src/app/pages/oauth/oauth.component.ts new file mode 100644 index 0000000..c9d4cbd --- /dev/null +++ b/src/app/pages/oauth/oauth.component.ts @@ -0,0 +1,71 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {UserService} from "../../modules/user/services/user.service"; +import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, Validators} from "@angular/forms"; +import {filter, map, switchMap, take, tap} from "rxjs/operators"; +import {Observable, timer} from "rxjs"; +import {ApiService} from "../../modules/api/services/api.service"; +import {Router} from "@angular/router"; + +function usernameIsNotTaken(apiService: ApiService) { + return (control: AbstractControl): Promise | Observable => { + return timer(500).pipe( + switchMap(() => apiService.usernameTaken(control.value)), + tap(v => console.log(v, v === true)), + map((res) => res ? {usernameIsTaken: true} : null) + ) + }; +} + +@Component({ + selector: 'rh-oauth', + templateUrl: './oauth.component.html', + styleUrls: ['./oauth.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OauthComponent { + + public readonly signedInUser$ = this.userService.signedInUser$; + + public readonly form: FormGroup; + public readonly usernameControl: FormControl; + + public constructor(private readonly userService: UserService, + private readonly fb: FormBuilder, + private readonly apiService: ApiService, + private readonly router: Router) { + this.usernameControl = this.fb.control('', [Validators.required], [usernameIsNotTaken(apiService)]); + + this.form = this.fb.group({ + username: this.usernameControl + }); + + this.signedInUser$.pipe( + filter((value => !!value)), + take(1) + ) + .subscribe((user) => { + if (!user) { + return; + } + + this.form.patchValue({username: user.oauthClients[0].providerUsername + user.oauthClients[0].providerUserDiscriminator}); + }); + } + + onSubmitForm() { + const username = this.usernameControl.value; + + this.apiService.changeUsername(username) + .subscribe((res) => { + this.userService.signedInUser$.next(res); + + if (res) { + this.router.navigate(['/browser']) + } + }); + } + + onLogout() { + this.userService.logout(); + } +} diff --git a/src/app/pages/oauth/oauth.module.ts b/src/app/pages/oauth/oauth.module.ts new file mode 100644 index 0000000..4e0db07 --- /dev/null +++ b/src/app/pages/oauth/oauth.module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {OauthComponent} from './oauth.component'; +import {OauthRoutingModule} from "./oauth-routing.module"; +import {OauthProviderUserBadgeComponent} from './oauth-provider-user-badge/oauth-provider-user-badge.component'; +import {ReactiveFormsModule} from "@angular/forms"; + +@NgModule({ + declarations: [ + OauthComponent, + OauthProviderUserBadgeComponent + ], + imports: [ + CommonModule, + OauthRoutingModule, + ReactiveFormsModule + ] +}) +export class OauthModule { +} diff --git a/src/assets/images/social/discord.png b/src/assets/images/social/discord.png new file mode 100644 index 0000000..396e497 Binary files /dev/null and b/src/assets/images/social/discord.png differ diff --git a/src/assets/images/xivrh-logo.png b/src/assets/images/xivrh-logo.png new file mode 100644 index 0000000..ab967f7 Binary files /dev/null and b/src/assets/images/xivrh-logo.png differ diff --git a/src/styles/_buttons.scss b/src/styles/_buttons.scss index 8b3ec28..4ceba58 100644 --- a/src/styles/_buttons.scss +++ b/src/styles/_buttons.scss @@ -33,6 +33,16 @@ a, text-align: center; } + &--filled { + background: #2f79a3; + color: white; + border: 1px solid #2f79a3; + + &:hover { + color: rgba(255, 255, 255, .8); + } + } + &--warning { color: darkred; border-color: darkred;