update routing client

This commit is contained in:
Kenta420-Poom 2023-09-18 08:50:13 +07:00
parent d7b7bc7be0
commit 218b2de59a
18 changed files with 377 additions and 148 deletions

View file

@ -1,11 +1,62 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NgModule, inject } from '@angular/core';
import { CanActivateFn, Router, RouterModule, Routes } from '@angular/router';
import { UserService } from './core/services/user.service';
import { map } from 'rxjs';
const authGuard: CanActivateFn = () => {
const userService: UserService = inject(UserService);
const router: Router = inject(Router);
return userService.isAuthenticated.pipe(
map((isAuth) => isAuth || router.parseUrl('/login'))
);
};
const loginGuard: CanActivateFn = () => {
const userService: UserService = inject(UserService);
const router: Router = inject(Router);
return userService.isAuthenticated.pipe(
map((isAuth) => {
if (!isAuth) {
return true;
}
return router.parseUrl('/dashboard');
})
);
};
const routes: Routes = [
{
path: 'login',
loadComponent: () =>
import('./core/auth/auth.component').then((m) => m.AuthComponent),
canActivate: [loginGuard],
},
{
path: 'register',
loadComponent: () =>
import('./core/auth/auth.component').then((m) => m.AuthComponent),
canActivate: [loginGuard],
},
{
path: '',
loadComponent: () =>
import('./features/home/home.component').then((m) => m.HomeComponent),
import('./core/layout/layout.component').then((m) => m.LayoutComponent),
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'dashboard',
},
{
path: 'dashboard',
loadComponent: () =>
import('./features/dashboard/dashboard.component').then(
(m) => m.DashboardComponent
),
canActivate: [authGuard],
},
],
},
];

View file

@ -1,5 +1 @@
<app-layout-header></app-layout-header>
<router-outlet></router-outlet>
<app-layout-footer></app-layout-footer>

View file

@ -2,16 +2,15 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {
GoogleLoginProvider,
GoogleSigninButtonModule,
SocialAuthServiceConfig,
SocialLoginModule,
} from '@abacritt/angularx-social-login';
import { CoreModule } from './core/core.module';
import { FooterComponent } from './core/layout/footer.component';
import { HeaderComponent } from './core/layout/header.component';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
@ -21,6 +20,7 @@ import { HeaderComponent } from './core/layout/header.component';
HeaderComponent,
AppRoutingModule,
SocialLoginModule,
HttpClientModule,
],
providers: [
{

View file

@ -0,0 +1,84 @@
<main class="flex flex-col justify-around">
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img
class="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"
>
Sign in to your account
</h2>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm flex justify-center">
<asl-google-signin-button
type="standard"
shape="pill"
text="signin_with"
size="large"
logo_alignment="center"
theme="outline"
width="300"
></asl-google-signin-button>
</div>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST">
<div>
<label
for="email"
class="block text-sm font-medium leading-6 text-gray-900"
>Email address</label
>
<div class="mt-2">
<input
id="email"
name="email"
type="email"
autocomplete="email"
required
class="px-3 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label
for="password"
class="block text-sm font-medium leading-6 text-gray-900"
>Password</label
>
<div class="text-sm">
<a
href="#"
class="font-semibold text-indigo-600 hover:text-indigo-500"
>Forgot password?</a
>
</div>
</div>
<div class="mt-2">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
class="px-3 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Sign in
</button>
</div>
</form>
</div>
</div>
</main>

View file

@ -1,7 +1,15 @@
import { NgIf } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Form, FormControl, FormGroup } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
import { Errors } from '../models/errors.model';
import { UserService } from '../services/user.service';
import {
GoogleSigninButtonModule,
SocialAuthService,
} from '@abacritt/angularx-social-login';
import { User } from '../models/user.model';
interface AuthForm {
email: FormControl<string>;
@ -12,30 +20,95 @@ interface AuthForm {
@Component({
selector: 'app-auth-page',
templateUrl: './auth.component.html',
imports: [RouterLink, NgIf],
imports: [RouterLink, NgIf, GoogleSigninButtonModule],
standalone: true,
})
export class AuthComponent implements OnInit {
export class AuthComponent implements OnInit, OnDestroy {
authType = '';
title = '';
errors: Errors = { errors: {} };
isSubmitting = false;
authForm: FormGroup<AuthForm>;
destroy$ = new Subject<void>();
constructor(
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly userService: UserService
private readonly userService: UserService,
private readonly authService: SocialAuthService
) {
// use FormBuilder to create a form group
this.authForm = new FormGroup<AuthForm>({
email: new FormControl('', {
validators: [Validators.required],
validators: [Validators.required, Validators.email],
nonNullable: true,
}),
password: new FormControl('', {
validators: [Validators.required],
validators: [Validators.required, Validators.minLength(8)],
nonNullable: true,
}),
});
}
ngOnInit(): void {
this.authType = this.route.snapshot.url.at(-1)!.path;
this.title = this.authType === 'login' ? 'Sign in' : 'Sign up';
if (this.authType === 'resigter') {
this.authForm.addControl(
'username',
new FormControl('', {
validators: [Validators.required],
nonNullable: true,
})
);
}
// google login
this.authService.authState.subscribe((user) => {
this.userService.setAuth({
username: user.name,
email: user.email,
token: user.idToken,
image: user.photoUrl,
bio: '',
} satisfies User);
void this.router.navigate(['/dashboard']);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
summitForm(): void {
this.isSubmitting = true;
this.errors = { errors: {} };
let observable;
if (this.authType === 'login') {
observable = this.userService.login(
this.authForm.value as {
email: string;
password: string;
}
);
} else {
observable = this.userService.register(
this.authForm.value as {
email: string;
password: string;
username: string;
}
);
}
observable.pipe(takeUntil(this.destroy$)).subscribe({
next: () => void this.router.navigate(['/login']),
error: (err: Errors) => {
this.errors = err;
this.isSubmitting = false;
},
});
}
}

View file

@ -1,8 +1,12 @@
import { NgIf } from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'app-layout-header',
templateUrl: './header.component.html',
imports: [NgIf],
standalone: true,
})
export class HeaderComponent {}
export class HeaderComponent {
constructor() {}
}

View file

@ -0,0 +1,5 @@
<app-layout-header></app-layout-header>
<router-outlet></router-outlet>
<app-layout-footer></app-layout-footer>

View file

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { HeaderComponent } from './header.component';
import { FooterComponent } from './footer.component';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-layout',
templateUrl: './layout.component.html',
standalone: true,
imports: [HeaderComponent, FooterComponent, RouterModule],
})
export class LayoutComponent {}

View file

@ -0,0 +1,3 @@
export interface Errors {
errors: { [key: string]: string };
}

View file

@ -0,0 +1,7 @@
export interface User {
email: string;
token: string;
username: string;
bio: string;
image: string;
}

View file

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class JwtService {
getToken(): string | null {
return window.localStorage['jwtToken'];
}
saveToken(token: string): void {
window.localStorage['jwtToken'] = token;
}
destroyToken(): void {
window.localStorage.removeItem('jwtToken');
}
}

View file

@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
BehaviorSubject,
Observable,
distinctUntilChanged,
map,
tap,
} from 'rxjs';
import { User } from '../models/user.model';
import { Router } from '@angular/router';
import { JwtService } from './jwt.service';
@Injectable({ providedIn: 'root' })
export class UserService {
private currnetUserSubject = new BehaviorSubject<User | null>(null);
public currentUser = this.currnetUserSubject
.asObservable()
.pipe(distinctUntilChanged());
public isAuthenticated = this.currentUser.pipe(
map((user) => {
return user !== null;
})
);
constructor(
private readonly http: HttpClient,
private readonly router: Router,
private readonly jwtService: JwtService
) {}
login(credentials: {
email: string;
password: string;
}): Observable<{ user: User }> {
return this.http
.post<{ user: User }>('/api/users/login', credentials)
.pipe(tap(({ user }) => this.setAuth(user)));
}
register(credentials: {
username: string;
email: string;
password: string;
}): Observable<{ user: User }> {
return this.http
.post<{ user: User }>('/api/users', { user: credentials })
.pipe(tap(({ user }) => this.setAuth(user)));
}
logout(): void {
this.purgeAuth();
void this.router.navigate(['/login']);
}
getCurrentUser(): Observable<{ user: User }> {
return this.http.get<{ user: User }>('/api/user').pipe(
tap({
next: ({ user }) => this.setAuth(user),
error: () => this.purgeAuth(),
})
);
}
update(user: Partial<User>): Observable<{ user: User }> {
return this.http
.put<{ user: User }>('/api/user', { user })
.pipe(tap(({ user }) => this.currnetUserSubject.next(user)));
}
setAuth(user: User): void {
this.jwtService.saveToken(user.token);
void this.currnetUserSubject.next(user);
}
purgeAuth(): void {
this.jwtService.destroyToken();
this.currnetUserSubject.next(null);
}
}

View file

@ -0,0 +1,4 @@
<p>dashboard works!</p>
<button (click)="logout()" class="bg-blue-300 rounded-lg min-w-fit p-10">
Logout
</button>

View file

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from 'src/app/core/services/user.service';
import { SocialAuthService } from '@abacritt/angularx-social-login';
import { Router } from '@angular/router';
@Component({
selector: 'app-dashboard',
standalone: true,
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
export class DashboardComponent {
constructor(private _userService: UserService) {}
logout() {
console.log('logout');
this._userService.logout();
// this._authService.signOut();
}
}

View file

@ -1,84 +0,0 @@
<main class="flex flex-col justify-around">
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img
class="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"
>
Sign in to your account
</h2>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<asl-google-signin-button
type="standard"
shape="pill"
text="signin_with"
size="large"
logo_alignment="center"
theme="outline"
width="400"
></asl-google-signin-button>
</div>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="#" method="POST">
<div>
<label
for="email"
class="block text-sm font-medium leading-6 text-gray-900"
>Email address</label
>
<div class="mt-2">
<input
id="email"
name="email"
type="email"
autocomplete="email"
required
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label
for="password"
class="block text-sm font-medium leading-6 text-gray-900"
>Password</label
>
<div class="text-sm">
<a
href="#"
class="font-semibold text-indigo-600 hover:text-indigo-500"
>Forgot password?</a
>
</div>
</div>
<div class="mt-2">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<button
type="submit"
class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Sign in
</button>
</div>
</form>
</div>
</div>
</main>

View file

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [HomeComponent]
});
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,23 +0,0 @@
import {
GoogleSigninButtonModule,
SocialAuthService,
} from '@abacritt/angularx-social-login';
import { Component, OnInit } from '@angular/core';
import { AppModule } from 'src/app/app.module';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css'],
imports: [GoogleSigninButtonModule],
standalone: true,
})
export class HomeComponent implements OnInit {
constructor(private _authService: SocialAuthService) {}
ngOnInit(): void {
this._authService.authState.subscribe((user) => {
console.log(user);
});
}
}