update: google oauth2.0 with hd=email@forth.co.th only now functional
This commit is contained in:
parent
984707c7bf
commit
36c71eda38
31 changed files with 580 additions and 317 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
client/src/app/core/callback/callback.component.html
Normal file
1
client/src/app/core/callback/callback.component.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!-- just call back nothing here -->
|
||||
37
client/src/app/core/callback/callback.component.ts
Normal file
37
client/src/app/core/callback/callback.component.ts
Normal 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(['/']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
31
client/src/app/core/interceptors/error.interceptor.ts
Normal file
31
client/src/app/core/interceptors/error.interceptor.ts
Normal 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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
export interface User {
|
||||
email: string;
|
||||
token: string;
|
||||
username: string;
|
||||
bio: string;
|
||||
image: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue