Feature/add account registration page (#141)

* Add account registration page

* Update changelog
pull/144/head
Thomas 3 years ago committed by GitHub
parent aa4206af0e
commit 026a5011d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Added a dedicated page for the account registration
### Changed
- Changed the buttons to links (`<a>`) on the tools page

@ -45,6 +45,13 @@ const routes: Routes = [
(m) => m.PricingPageModule
)
},
{
path: 'register',
loadChildren: () =>
import('./pages/register/register-page.module').then(
(m) => m.RegisterPageModule
)
},
{
path: 'resources',
loadChildren: () =>
@ -55,7 +62,9 @@ const routes: Routes = [
{
path: 'start',
loadChildren: () =>
import('./pages/login/login-page.module').then((m) => m.LoginPageModule)
import('./pages/landing/landing-page.module').then(
(m) => m.LandingPageModule
)
},
{
path: 'tools',

@ -12,13 +12,15 @@
<div *ngIf="canCreateAccount" class="container create-account-container">
<div class="row mb-5">
<div class="col-md-6 offset-md-3">
<div
class="create-account-box p-2 text-center"
(click)="onCreateAccount()"
<a [routerLink]="['/']">
<mat-card
class="create-account-box p-2 text-center"
(click)="onCreateAccount()"
>
<div class="mt-1" i18n>You are using the Live Demo.</div>
<button mat-button color="primary" i18n>Create Account</button>
</mat-card></a
>
<div class="mt-1" i18n>You are using the Live Demo.</div>
<button mat-button color="primary" i18n>Create Account</button>
</div>
</div>
</div>
</div>

@ -5,8 +5,6 @@
padding: 5rem 0;
.create-account-box {
border: 1px solid rgba(var(--palette-primary-500), 1);
border-radius: 0.25rem;
cursor: pointer;
font-size: 90%;

@ -68,17 +68,12 @@ export class AppComponent implements OnDestroy, OnInit {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.canCreateAccount = hasPermission(
this.user.permissions,
permissions.createUserAccount
);
} else if (!this.tokenStorageService.getToken()) {
// User has not been logged in
this.user = null;
}
this.user = state.user;
this.canCreateAccount = hasPermission(
this.user?.permissions,
permissions.createUserAccount
);
this.changeDetectorRef.markForCheck();
});
@ -86,7 +81,6 @@ export class AppComponent implements OnDestroy, OnInit {
public onCreateAccount() {
this.tokenStorageService.signOut();
window.location.reload();
}
public onSignOut() {

@ -2,6 +2,7 @@ import { Platform } from '@angular/cdk/platform';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import {
DateAdapter,
MAT_DATE_FORMATS,
@ -34,6 +35,7 @@ import { LanguageService } from './core/language.service';
HttpClientModule,
MarkdownModule.forRoot(),
MatButtonModule,
MatCardModule,
MaterialCssVarsModule.forRoot({
darkThemeClass: 'is-dark-theme',
isAutoContrast: true,

@ -8,9 +8,11 @@
class="d-none d-sm-block"
i18n
mat-flat-button
[color]="
currentRoute === 'home' || currentRoute === 'zen' ? 'primary' : null
"
[ngClass]="{
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen',
'text-decoration-underline':
currentRoute === 'home' || currentRoute === 'zen'
}"
[routerLink]="['/']"
>Overview</a
>
@ -19,13 +21,16 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="
currentRoute === 'analysis' ||
currentRoute === 'report' ||
currentRoute === 'tools'
? 'primary'
: null
"
[ngClass]="{
'font-weight-bold':
currentRoute === 'analysis' ||
currentRoute === 'report' ||
currentRoute === 'tools',
'text-decoration-underline':
currentRoute === 'analysis' ||
currentRoute === 'report' ||
currentRoute === 'tools'
}"
[routerLink]="['/tools']"
>Tools</a
>
@ -33,7 +38,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'transactions' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'transactions',
'text-decoration-underline': currentRoute === 'transactions'
}"
[routerLink]="['/transactions']"
>Transactions</a
>
@ -41,7 +49,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'accounts' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'accounts',
'text-decoration-underline': currentRoute === 'accounts'
}"
[routerLink]="['/accounts']"
>Accounts</a
>
@ -50,7 +61,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'admin' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'admin',
'text-decoration-underline': currentRoute === 'admin'
}"
[routerLink]="['/admin']"
>Admin Control</a
>
@ -58,7 +72,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'resources' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'resources',
'text-decoration-underline': currentRoute === 'resources'
}"
[routerLink]="['/resources']"
>Resources</a
>
@ -67,7 +84,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'pricing' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'pricing',
'text-decoration-underline': currentRoute === 'pricing'
}"
[routerLink]="['/pricing']"
>Pricing</a
>
@ -75,7 +95,10 @@
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'about' ? 'primary' : null"
[ngClass]="{
'font-weight-bold': currentRoute === 'about',
'text-decoration-underline': currentRoute === 'about'
}"
[routerLink]="['/about']"
>About</a
>
@ -227,27 +250,43 @@
</a>
<span class="spacer"></span>
<a
*ngIf="hasPermissionForSubscription"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[color]="currentRoute === 'pricing' ? 'primary' : null"
[routerLink]="['/pricing']"
>Pricing</a
[ngClass]="{
'font-weight-bold': currentRoute === 'about',
'text-decoration-underline': currentRoute === 'about'
}"
[routerLink]="['/about']"
>About</a
>
<a
class="d-none d-sm-block mx-1"
*ngIf="hasPermissionForSubscription"
i18n
mat-flat-button
[color]="currentRoute === 'about' ? 'primary' : null"
[routerLink]="['/about']"
>About</a
[ngClass]="{
'font-weight-bold': currentRoute === 'pricing',
'text-decoration-underline': currentRoute === 'pricing'
}"
[routerLink]="['/pricing']"
>Pricing</a
>
<a
class="d-none d-sm-block mx-1"
class="d-none d-sm-block mx-1 no-min-width px-1"
href="https://github.com/ghostfolio/ghostfolio"
mat-flat-button
>GitHub</a
>
<button i18n mat-flat-button (click)="openLoginDialog()">Sign in</button>
><ion-icon name="logo-github"></ion-icon
></a>
<button class="mx-1" i18n mat-flat-button (click)="openLoginDialog()">
Sign In
</button>
<a
class="d-none d-sm-block"
color="primary"
i18n
mat-flat-button
[routerLink]="['/register']"
>Get Started
</a>
</ng-container>
</mat-toolbar>

@ -8,7 +8,7 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';

@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router';
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { GfLogoModule } from '../logo/logo.module';
import { HeaderComponent } from './header.component';

@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'login-with-access-token-dialog',
selector: 'gf-login-with-access-token-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./login-with-access-token-dialog.scss'],
templateUrl: 'login-with-access-token-dialog.html'

@ -14,7 +14,12 @@ import { UserService } from '../services/user/user.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
private static PUBLIC_PAGE_ROUTES = ['/about', '/pricing', '/resources'];
private static PUBLIC_PAGE_ROUTES = [
'/about',
'/pricing',
'/register',
'/resources'
];
constructor(
private router: Router,

@ -2,14 +2,14 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { LoginPageComponent } from './login-page.component';
import { LandingPageComponent } from './landing-page.component';
const routes: Routes = [
{ path: '', component: LoginPageComponent, canActivate: [AuthGuard] }
{ path: '', component: LandingPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LoginPageRoutingModule {}
export class LandingPageRoutingModule {}

@ -1,21 +1,17 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
@Component({
selector: 'gf-login-page',
templateUrl: './login-page.html',
styleUrls: ['./login-page.scss']
selector: 'gf-landing-page',
templateUrl: './landing-page.html',
styleUrls: ['./landing-page.scss']
})
export class LoginPageComponent implements OnDestroy, OnInit {
export class LandingPageComponent implements OnDestroy, OnInit {
public currentYear = format(new Date(), 'yyyy');
public demoAuthToken: string;
public historicalDataItems: LineChartItem[];
@ -28,7 +24,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private dialog: MatDialog,
private router: Router,
private tokenStorageService: TokenStorageService
) {}
@ -46,15 +41,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
});
}
public async createAccount() {
this.dataService
.postUser()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accessToken, authToken }) => {
this.openShowAccessTokenDialog(accessToken, authToken);
});
}
public initializeLineChart() {
this.historicalDataItems = [
{
@ -268,28 +254,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
];
}
public openShowAccessTokenDialog(
accessToken: string,
authToken: string
): void {
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
data: {
accessToken,
authToken
},
disableClose: true,
width: '30rem'
});
dialogRef.afterClosed().subscribe((data) => {
if (data?.authToken) {
this.tokenStorageService.saveToken(authToken);
this.router.navigate(['/']);
}
});
}
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken);

@ -13,16 +13,16 @@
class="align-items-center col d-flex justify-content-center position-relative"
>
<div class="py-5 text-center">
<button
<a
class="d-inline-block"
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
(click)="createAccount()"
[routerLink]="['/register']"
>
Create Account
</button>
Get Started
</a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
<button
class="d-inline-block"
@ -135,15 +135,15 @@
Join now or check out the example account
</p>
<div class="py-2 text-center">
<button
<a
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
(click)="createAccount()"
[routerLink]="['/register']"
>
Create Account
</button>
Get Started
</a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
<button
class="d-inline-block"

@ -0,0 +1,25 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
import { LandingPageRoutingModule } from './landing-page-routing.module';
import { LandingPageComponent } from './landing-page.component';
@NgModule({
declarations: [LandingPageComponent],
exports: [],
imports: [
CommonModule,
GfLineChartModule,
GfLogoModule,
LandingPageRoutingModule,
MatButtonModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LandingPageModule {}

@ -1,7 +1,9 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Pricing Plans</h3>
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
Pricing Plans
</h3>
<mat-card class="mb-4">
<mat-card-content>
<p>
@ -188,8 +190,8 @@
</div>
<div *ngIf="!user" class="row">
<div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="['/start']">
Create Account
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
Get Started
</a>
<p class="text-muted"><small>It's free</small></p>
</div>

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { RegisterPageComponent } from './register-page.component';
const routes: Routes = [
{ path: '', component: RegisterPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class RegisterPageRoutingModule {}

@ -0,0 +1,98 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
@Component({
selector: 'gf-register-page',
templateUrl: './register-page.html',
styleUrls: ['./register-page.scss']
})
export class RegisterPageComponent implements OnDestroy, OnInit {
public currentYear = format(new Date(), 'yyyy');
public demoAuthToken: string;
public hasPermissionForSocialLogin: boolean;
public historicalDataItems: LineChartItem[];
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private dialog: MatDialog,
private router: Router,
private tokenStorageService: TokenStorageService
) {
this.tokenStorageService.signOut();
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.dataService
.fetchInfo()
.subscribe(({ demoAuthToken, globalPermissions }) => {
this.demoAuthToken = demoAuthToken;
this.hasPermissionForSocialLogin = hasPermission(
globalPermissions,
permissions.enableSocialLogin
);
this.changeDetectorRef.markForCheck();
});
}
public async createAccount() {
this.dataService
.postUser()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accessToken, authToken }) => {
this.openShowAccessTokenDialog(accessToken, authToken);
});
}
public openShowAccessTokenDialog(
accessToken: string,
authToken: string
): void {
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
data: {
accessToken,
authToken
},
disableClose: true,
width: '30rem'
});
dialogRef.afterClosed().subscribe((data) => {
if (data?.authToken) {
this.tokenStorageService.saveToken(authToken);
this.router.navigate(['/']);
}
});
}
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken);
this.router.navigate(['/']);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

@ -0,0 +1,30 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-flex justify-content-center mb-3 text-center" i18n>
Create your Ghostfolio account
</h3>
<mat-card class="mb-4">
<mat-card-content class="text-center">
<button
class="d-inline-block"
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
(click)="createAccount()"
>
Create Account
</button>
<ng-container *ngIf="hasPermissionForSocialLogin">
<div class="my-3 text-muted" i18n>or</div>
<a color="accent" href="/api/auth/google" mat-flat-button
><ion-icon class="mr-1" name="logo-google"></ion-icon
><span i18n>Continue with Google</span></a
>
</ng-container>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

@ -1,27 +1,27 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
import { LoginPageRoutingModule } from './login-page-routing.module';
import { LoginPageComponent } from './login-page.component';
import { RegisterPageRoutingModule } from './register-page-routing.module';
import { RegisterPageComponent } from './register-page.component';
import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-access-token-dialog.module';
@NgModule({
declarations: [LoginPageComponent],
declarations: [RegisterPageComponent],
exports: [],
imports: [
CommonModule,
GfLineChartModule,
GfLogoModule,
LoginPageRoutingModule,
MatButtonModule,
MatCardModule,
RegisterPageRoutingModule,
RouterModule,
ShowAccessTokenDialogModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LoginPageModule {}
export class RegisterPageModule {}

@ -7,7 +7,7 @@ import {
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'show-access-token-dialog',
selector: 'gf-show-access-token-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./show-access-token-dialog.scss'],
templateUrl: 'show-access-token-dialog.html'

@ -1,5 +1,5 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@ -1,12 +1,14 @@
import { Injectable } from '@angular/core';
import { UserService } from './user/user.service';
const TOKEN_KEY = 'auth-token';
@Injectable({
providedIn: 'root'
})
export class TokenStorageService {
public constructor() {}
public constructor(private userService: UserService) {}
public getToken(): string {
return window.localStorage.getItem(TOKEN_KEY);
@ -22,6 +24,8 @@ export class TokenStorageService {
window.localStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}

@ -6,18 +6,22 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about</loc>
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pricing</loc>
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/register</loc>
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/resources</loc>
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
<lastmod>2021-06-03T00:00:00+00:00</lastmod>
</url>
</urlset>

@ -153,3 +153,7 @@ ngx-skeleton-loader {
.no-min-width {
min-width: unset !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}

Loading…
Cancel
Save