Skip to content

Commit

Permalink
Login with username and password (#70)
Browse files Browse the repository at this point in the history
* initial login UI

* Login is taking shape

* Normal login works

* Add way to check for expired tokens

* Add method to check if we should renew the token

* MFA WORKS

* translate 2fa modal

* Better 2fa input fields

* Better button title

* update translations

* Start on new sign-up page

* Create component for pronouns input

* Create component for languages input

* Add country input component

* Finish register ui

* Add thank you section

* Start on nag about verifying emails

* Allow for re-sending verification email

* translate message for email verification

* WIP password resetting

* Proxy api to make it more like prod

* Translation update

* Request password reset from user settings

* Password reset from login system done

* Cleanup and bugfixing

* Add types to sync handler

* Ensure youtube works in new settings page
  • Loading branch information
duncte123 authored Mar 3, 2024
1 parent 662ac55 commit 23cc919
Show file tree
Hide file tree
Showing 64 changed files with 1,565 additions and 293 deletions.
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "webapp:build"
"browserTarget": "webapp:build",
"proxyConfig": "src/proxy.conf.json"
},
"configurations": {
"production": {
Expand Down
36 changes: 36 additions & 0 deletions languagePropertyCopier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Simple script that helps me copy translations and save our translators some time if I'm just copying.

const fs = require('fs');
const path = require('path');

const rootDir = path.resolve(__dirname, 'src', 'assets', 'i18n');
const jsonFiles = fs.readdirSync(rootDir).filter((file) => file.endsWith('.json'));

for (const file of jsonFiles) {
const filePath = path.resolve(rootDir, file);
const json = fs.readFileSync(filePath, 'utf8');
const obj = JSON.parse(json);

if (obj.navbar?.login?.discord) {
obj.login = { ...obj.login };
obj.login.provider = {...obj.login.provider };

obj.login.provider.discord = obj.navbar.login.discord;
}

if (obj.navbar?.login?.twitch) {
obj.login = { ...obj.login };
obj.login.provider = {...obj.login.provider };

obj.login.provider.twitch = obj.navbar.login.twitch;
}

if (obj.navbar?.login?.google) {
obj.login = { ...obj.login };
obj.login.provider = {...obj.login.provider };

obj.login.provider.google = obj.navbar.login.google;
}

fs.writeFileSync(filePath, JSON.stringify(obj, null, 2));
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@
<app-header-language-picker class="navbar-item pr-0"></app-header-language-picker>
<app-header-bar-user *ngIf="userService.user; else loggedOut" class="navbar-item pl-0"></app-header-bar-user>
<ng-template #loggedOut>
<app-widget-signin-picker type="NAVBAR" class="pl-0" [isRight]="true"></app-widget-signin-picker>
<a [routerLink]="'/login' | localize" (click)="storeCurrentLocation()" class="navbar-item">
{{ 'navbar.login.title' | translate }}
</a>
<a [routerLink]="'/register' | localize" class="navbar-item">
{{ 'navbar.register' | translate }}
</a>
</ng-template>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,23 @@ export class HeaderBarNavComponent {
toggleNavbarActive(): void {
this.isNavbarActive = !this.isNavbarActive;
}

get noRedirectPaths(): string[] {
return [
'/register',
'/forgot-password'
];
}

storeCurrentLocation(): boolean {
let path = window.location.pathname;

if (this.noRedirectPaths.includes(path)) {
path = '/';
}

// We store pathname because "403" can be inserted if a user is not logged in
localStorage.setItem('prev_loc', path);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div *ngIf="show" class="verify-host">
<div>
{{ 'navbar.email-verification.message' | translate }}
</div>

<div class="buttons">
<button type="button" class="button is-info" [disabled]="!canPressButton" (click)="requestNewEmail()">
{{ 'navbar.email-verification.request-btn' | translate }}
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.verify-host {
padding: 10px;
}

div {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.5em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Component } from '@angular/core';
import { UserService } from '../../../../services/user.service';
import { AuthService } from '../../../../services/auth.service';

@Component({
selector: 'app-header-bar-verify-email',
templateUrl: './header-bar-verify-email.component.html',
styleUrls: ['./header-bar-verify-email.component.scss']
})
export class HeaderBarVerifyEmailComponent {

canPressButton = true;

constructor(
private authService: AuthService,
private userService: UserService,
) { }

requestNewEmail() {
this.canPressButton = false;
this.authService.requestNewVerificationEmail().subscribe({
next({ status }) {
console.log(status);
alert('Email sent!');
},

error(err: any) {
console.log(err);
alert('Something went wrong! Please try again later. ');
}
});
}

get show(): boolean {
return this.userService.user && !this.userService.user.emailVerified;
}

}
1 change: 1 addition & 0 deletions src/app/_layout/header-bar/header-bar.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<header>
<app-header-bar-display-name></app-header-bar-display-name>
<app-header-bar-verify-email></app-header-bar-verify-email>
<app-header-bar-cookies *ngIf="showCookieBar" (visibilityUpdated)="showCookieBar = $event"></app-header-bar-cookies>
<app-header-bar-nav></app-header-bar-nav>
</header>
24 changes: 20 additions & 4 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { OldLoginComponent } from './login/old-login.component';
import { HttpClientModule } from '@angular/common/http';
import { httpInterceptorProviders } from '../interceptors';
import { FormsModule } from '@angular/forms';
Expand Down Expand Up @@ -54,6 +54,10 @@ import { availableLocaleNames } from '../services/locale.service';
import { HeaderBarUserComponent } from './_layout/header-bar/header-bar-user/header-bar-user.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { HeaderBarDisplayNameComponent } from './_layout/header-bar/header-bar-display-name/header-bar-display-name.component';
import { LoginComponent } from './auth/login/login.component';
import { HeaderBarVerifyEmailComponent } from './_layout/header-bar/header-bar-verify-email/header-bar-verify-email.component';
import { PasswordResetComponent } from './auth/password-reset/password-reset.component';
import { ForgotPasswordComponent } from './auth/forgot-password/forgot-password.component';

const appRoutes: Routes = [
{
Expand All @@ -62,11 +66,19 @@ const appRoutes: Routes = [
},
{
path: 'login',
component: SignUpComponent,
component: LoginComponent,
},
{
path: 'password-reset/:token',
component: PasswordResetComponent,
},
{
path: 'forgot-password',
component: ForgotPasswordComponent,
},
{
path: 'login/:service',
component: LoginComponent,
component: OldLoginComponent,
data: { skipRouteLocalization: true },
},
{
Expand Down Expand Up @@ -113,7 +125,7 @@ const appRoutes: Routes = [
@NgModule({
declarations: [
AppComponent,
LoginComponent,
OldLoginComponent,
HomepageComponent,
AboutComponent,
PatronsComponent,
Expand All @@ -122,6 +134,7 @@ const appRoutes: Routes = [
KasperskyAnnouncementComponent,
PlumComponent,
SignUpComponent,
LoginComponent,
WelcomeComponent,
SponsorsComponent,
MarathonsComponent,
Expand All @@ -141,6 +154,9 @@ const appRoutes: Routes = [
HeaderBarUserComponent,
UnauthorizedComponent,
HeaderBarDisplayNameComponent,
HeaderBarVerifyEmailComponent,
PasswordResetComponent,
ForgotPasswordComponent,
],
imports: [
BrowserModule,
Expand Down
36 changes: 36 additions & 0 deletions src/app/auth/forgot-password/forgot-password.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<section class="section container">
<h1 class="title">Reset your password!</h1>

<div class="notification" [class]="notificationClass" *ngIf="errorTranslationKey">
<button class="delete" (click)="errorTranslationKey = null"></button>
{{ errorTranslationKey | translate }}
</div>

<p>{{ 'auth.passwordReset.instructions' | translate }}</p>

<div class="columns">
<div class="column is-three-fifths is-offset-one-fifth">
<form (submit)="requestNewPassword($event.target)">
<fieldset [disabled]="loading">

<div class="field">
<label class="label">{{ 'user.settings.email.label' | translate }}</label>
<div class="control has-icons-left">
<input class="input" required type="email" placeholder="[email protected]" name="email" [(ngModel)]="email">
<span class="icon is-small is-left">
<fa-icon [icon]="iconUser"></fa-icon>
</span>
</div>
</div>

<div class="control">
<button type="submit" style="margin-right: 1rem"
[ngClass]="{'is-loading': loading}"
class="button is-primary">{{ 'auth.passwordReset.request' | translate }}</button>
</div>

</fieldset>
</form>
</div>
</div>
</section>
File renamed without changes.
55 changes: 55 additions & 0 deletions src/app/auth/forgot-password/forgot-password.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Component } from '@angular/core';
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
import { passwordResetErrorToMessage } from '../../../utils/authHelpers';
import { firstValueFrom } from 'rxjs';
import { AuthService } from '../../../services/auth.service';

@Component({
selector: 'app-forgot-password',
templateUrl: './forgot-password.component.html',
styleUrls: ['./forgot-password.component.scss']
})
export class ForgotPasswordComponent {
iconUser = faEnvelope;

errorTranslationKey: string | null = null;
notificationClass = 'is-danger';

loading = false;
email = '';

constructor(
private authService: AuthService,
) { }

async requestNewPassword(form: HTMLFormElement) {
if (!form.reportValidity()) {
return;
}

try {
this.loading = true;

const { status } = await firstValueFrom(this.authService.requestPasswordReset(this.email));

if (status === 'PASSWORD_RESET_SENT') {
this.notificationClass = 'is-success';
this.errorTranslationKey = 'auth.passwordReset.requested';
this.email = '';
} else {
this.errorTranslationKey = 'You should never see this message. If you do, let me know what you did.';
}

} catch (e: any) {
console.log(e.error);
this.notificationClass = 'is-danger';
this.errorTranslationKey = passwordResetErrorToMessage(e);
} finally {
this.loading = false;
}
}

get title(): string {
return 'Forgot your password';
}
}
81 changes: 80 additions & 1 deletion src/app/auth/login/login.component.html
Original file line number Diff line number Diff line change
@@ -1 +1,80 @@
<p>login works!</p>
<section class="section container">
<h1 class="title">{{ 'login.title' | translate }}</h1>

<div class="notification is-danger" *ngIf="loginError == 'USERNAME_PASSWORD_INCORRECT'">
<button class="delete" (click)="loginError = null"></button>
The entered username and password are incorrect.
</div>

<div class="notification is-danger" *ngIf="loginError == 'MFA_INVALID'">
<button class="delete" (click)="loginError = null"></button>
2FA code is not valid.
</div>

<div class="columns">
<div class="column is-three-fifths is-offset-one-fifth">
<form action="">

<fieldset [disabled]="loading">
<div class="field">
<label class="label">{{ 'user.settings.username.label' | translate }}</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="OengusIO" name="username" [(ngModel)]="loginData.username">
<span class="icon is-small is-left">
<fa-icon [icon]="iconUser"></fa-icon>
</span>
</div>
</div>

<app-element-password-input [(password)]="loginData.password"></app-element-password-input>

<div class="field" *ngIf="mfaNeeded">
<label class="label">{{ 'login.2fa.label' | translate }}</label>
<div class="control">
<input class="input"
type="text"
placeholder="000000"
inputmode="numeric"
pattern="[0-9]*"
name="2fa"
[(ngModel)]="loginData.twoFactorCode" />
</div>
</div>

<div class="control">
<button type="submit" style="margin-right: 1rem"
[ngClass]="{'is-loading': loading}"
(click)="performLogin()"
class="button is-primary">{{'login.button' | translate}}</button>

<a [routerLink]="'/forgot-password' | localize">{{ 'login.forgotPass' | translate }}</a>
</div>
</fieldset>

</form>
</div>
</div>

<hr/>
<h2 class="subtitle">
{{ 'login.provider.title' | translate }}
</h2>

<div class="buttons is-centered are-large">
<a [href]="authService.getDiscordAuthUri()" class="button is-discord">
<span class="icon">
<fa-icon [icon]="iconDiscord"></fa-icon>
</span>
<span>{{ 'login.provider.discord' | translate }}</span>
</a>
<a [href]="authService.getTwitchAuthUrl()" class="button is-twitch">
<span class="icon">
<fa-icon [icon]="iconTwitch"></fa-icon>
</span>
<span>{{ 'login.provider.twitch' | translate }}</span>
</a>
<!--<button class="button is-static">
{{ 'login.provider.google' | translate }}
</button>-->
</div>
</section>
1 change: 1 addition & 0 deletions src/app/auth/login/login.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading

0 comments on commit 23cc919

Please sign in to comment.