update: google oauth2.0 with hd=email@forth.co.th only now functional

This commit is contained in:
Kenta420-Poom 2023-09-20 09:57:47 +07:00
parent 984707c7bf
commit 36c71eda38
31 changed files with 580 additions and 317 deletions

View file

@ -1,13 +1,19 @@
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';
import { Subject, finalize, lastValueFrom, map, takeUntil } 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'))
map((isAuth) => {
if (isAuth) {
return true;
}
return router.parseUrl('/login');
})
);
};
@ -15,14 +21,16 @@ const loginGuard: CanActivateFn = () => {
const userService: UserService = inject(UserService);
const router: Router = inject(Router);
return userService.isAuthenticated.pipe(
map((isAuth) => {
if (!isAuth) {
return true;
return lastValueFrom(userService.getCurrentUser())
.then(({ user }) => {
if (user) {
return router.parseUrl('/dashboard');
}
return router.parseUrl('/dashboard');
return true;
})
);
.catch(() => {
return true;
});
};
const routes: Routes = [
@ -33,10 +41,11 @@ const routes: Routes = [
canActivate: [loginGuard],
},
{
path: 'register',
path: 'callback',
loadComponent: () =>
import('./core/auth/auth.component').then((m) => m.AuthComponent),
canActivate: [loginGuard],
import('./core/callback/callback.component').then(
(m) => m.CallbackComponent
),
},
{
path: '',
@ -59,9 +68,10 @@ const routes: Routes = [
],
},
{
path: 'log',
loadComponent: () => import('./features/changelog/changelog.component').then((m) => m.ChangelogComponent),
}
path: '**',
pathMatch: 'full',
redirectTo: 'dashboard',
},
];
@NgModule({

View file

@ -1,7 +1,3 @@
import {
GoogleLoginProvider,
SocialAuthService,
} from '@abacritt/angularx-social-login';
import { Component, OnInit } from '@angular/core';
@Component({

View file

@ -2,11 +2,6 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import {
GoogleLoginProvider,
SocialAuthServiceConfig,
SocialLoginModule,
} from '@abacritt/angularx-social-login';
import { FooterComponent } from './core/layout/footer.component';
import { HeaderComponent } from './core/layout/header.component';
import { HttpClientModule } from '@angular/common/http';
@ -19,30 +14,8 @@ import { AppComponent } from './app.component';
FooterComponent,
HeaderComponent,
AppRoutingModule,
SocialLoginModule,
HttpClientModule,
],
providers: [
{
provide: 'SocialAuthServiceConfig',
useValue: {
autoLogin: false,
providers: [
{
id: GoogleLoginProvider.PROVIDER_ID,
provider: new GoogleLoginProvider(
'250904650832-atnankrca4pvegjofnp24hmefjke4doq.apps.googleusercontent.com',
{
oneTapEnabled: true,
scopes: 'profile email',
prompt: 'select_account',
}
),
},
],
} as SocialAuthServiceConfig,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}

View file

@ -1,84 +1,59 @@
<main class="flex flex-col justify-around">
<main class="flex flex-col justify-around h-[100vh] bg-[#EAE6E1]">
<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"
class="mx-auto h-[200px] w-auto"
src="/assets/logo.png"
alt="Tao Bin | Forth Vanding Machine"
/>
<h2
class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"
>
Sign in to your account
Sign in with your @Forth account
</h2>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm flex justify-center">
<asl-google-signin-button
<!-- <div id="signin_google" #googleLoginButton></div> -->
<button
class="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
(click)="loginWithGoogle()"
>
<img
class="w-6 h-6"
src="assets/google-color.svg"
alt="google logo"
/>
<span>Login with @foth.co.th Google account</span>
</button>
<!-- <asl-google-signin-button
type="standard"
shape="pill"
text="signin_with"
size="large"
logo_alignment="center"
theme="outline"
width="300"
></asl-google-signin-button>
width="400"
></asl-google-signin-button> -->
<!-- <div
id="g_id_onload"
data-client_id="250904650832-atnankrca4pvegjofnp24hmefjke4doq.apps.googleusercontent.com"
data-context="use"
data-ux_mode="popup"
data-callback="handleCredentialResponse"
data-itp_support="true"
data-moment_callback="handleMoment"
></div>
<div
class="g_id_signin"
data-type="standard"
data-shape="pill"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-logo_alignment="center"
data-width="400"
></div> -->
</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,79 +1,26 @@
import { NgIf } from '@angular/common';
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 { Router, RouterLink } from '@angular/router';
import { Subject } 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>;
password: FormControl<string>;
username?: FormControl<string>;
}
import jwtDecode from 'jwt-decode';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-auth-page',
templateUrl: './auth.component.html',
imports: [RouterLink, NgIf, GoogleSigninButtonModule],
imports: [RouterLink, NgIf],
standalone: true,
})
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 authService: SocialAuthService
) {
this.authForm = new FormGroup<AuthForm>({
email: new FormControl('', {
validators: [Validators.required, Validators.email],
nonNullable: true,
}),
password: new FormControl('', {
validators: [Validators.required, Validators.minLength(8)],
nonNullable: true,
}),
});
}
constructor() {}
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']);
});
this.title = 'Sign in';
}
ngOnDestroy(): void {
@ -81,34 +28,8 @@ export class AuthComponent implements OnInit, OnDestroy {
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;
},
});
loginWithGoogle(): void {
// redirect to google login in server
window.location.href = 'http://localhost:8080/auth/google';
}
}

View file

@ -0,0 +1 @@
<!-- just call back nothing here -->

View file

@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-callback',
standalone: true,
templateUrl: './callback.component.html',
})
export class CallbackComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {}
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
console.log(params);
if (params['email'] && params['name'] && params['picture']) {
this.userService.setAuth({
email: params['email'],
username: params['name'],
image: params['picture'],
});
}
if (params['redirect_to']) {
this.router.navigate([params['redirect_to']]);
} else {
this.router.navigate(['/']);
}
});
}
}

View file

@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
} from '@angular/common/http';
import { Observable, catchError, retry, throwError } from 'rxjs';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor() {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
catchError((error) => {
if (error instanceof HttpErrorResponse) {
if (error.status === 401) {
return next.handle(request);
}
}
return throwError(() => error);
})
);
}
}

View file

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

View file

@ -3,13 +3,14 @@ import { HttpClient } from '@angular/common/http';
import {
BehaviorSubject,
Observable,
concat,
distinctUntilChanged,
map,
tap,
} from 'rxjs';
import { User } from '../models/user.model';
import { Router } from '@angular/router';
import { JwtService } from './jwt.service';
import { environment } from 'src/environments/environment';
@Injectable({ providedIn: 'root' })
export class UserService {
@ -18,64 +19,45 @@ export class UserService {
.asObservable()
.pipe(distinctUntilChanged());
public isAuthenticated = this.currentUser.pipe(
map((user) => {
return user !== null;
})
);
public isAuthenticated = this.currentUser.pipe(map((user) => !!user));
constructor(
private readonly http: HttpClient,
private readonly router: Router,
private readonly jwtService: JwtService
private readonly router: Router
) {}
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']);
// post to api /revoke with cookie
this.http
.get(environment.api + '/auth/revoke', {
withCredentials: true,
})
.subscribe({
complete: () => this.router.navigateByUrl('/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)));
.get<{ user: User }>(environment.api + '/auth/user', {
withCredentials: true,
})
.pipe(
tap({
next: ({ user }) => {
this.setAuth(user);
},
error: () => this.purgeAuth(),
})
);
}
setAuth(user: User): void {
this.jwtService.saveToken(user.token);
void this.currnetUserSubject.next(user);
}
purgeAuth(): void {
this.jwtService.destroyToken();
this.currnetUserSubject.next(null);
void this.currnetUserSubject.next(null);
}
}

View file

@ -1,4 +1,9 @@
<p>dashboard works!</p>
<div *ngIf="userInfo">
<img [src]="userInfo.image" />
<p>Hi {{ userInfo.username }}</p>
<p>Your email is {{ userInfo.email }}</p>
</div>
<button (click)="logout()" class="bg-blue-300 rounded-lg min-w-fit p-10">
Logout
</button>

View file

@ -1,21 +1,29 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { UserService } from 'src/app/core/services/user.service';
import { SocialAuthService } from '@abacritt/angularx-social-login';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { User } from 'src/app/core/models/user.model';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [NgIf],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css'],
})
export class DashboardComponent {
export class DashboardComponent implements OnInit {
userInfo: User | null = null;
constructor(private _userService: UserService) {}
ngOnInit(): void {
this._userService.currentUser.subscribe((user) => {
this.userInfo = user;
});
}
logout() {
console.log('logout');
this._userService.logout();
// this._authService.signOut();
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Google-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-401.000000, -860.000000)">
<g id="Google" transform="translate(401.000000, 860.000000)">
<path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05">
</path>
<path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335">
</path>
<path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853">
</path>
<path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
client/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,4 @@
export const environment = {
production: false,
api: 'http://localhost:8080',
};

View file

@ -0,0 +1,4 @@
export const environment = {
production: true,
api: '',
};

View file

@ -6,6 +6,7 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script src="https://accounts.google.com/gsi/client" async></script>
</head>
<body>
<app-root></app-root>