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
|
|
@ -51,7 +51,13 @@
|
|||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
"namedChunks": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
|
|
|||
26
client/package-lock.json
generated
26
client/package-lock.json
generated
|
|
@ -8,7 +8,6 @@
|
|||
"name": "client",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@abacritt/angularx-social-login": "^2.1.0",
|
||||
"@angular/animations": "^16.2.0",
|
||||
"@angular/common": "^16.2.0",
|
||||
"@angular/compiler": "^16.2.0",
|
||||
|
|
@ -17,6 +16,7 @@
|
|||
"@angular/platform-browser": "^16.2.0",
|
||||
"@angular/platform-browser-dynamic": "^16.2.0",
|
||||
"@angular/router": "^16.2.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.0"
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"@angular-devkit/build-angular": "^16.2.2",
|
||||
"@angular/cli": "~16.2.2",
|
||||
"@angular/compiler-cli": "^16.2.0",
|
||||
"@types/google.accounts": "^0.0.9",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"jasmine-core": "~4.6.0",
|
||||
|
|
@ -38,18 +39,6 @@
|
|||
"typescript": "~5.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@abacritt/angularx-social-login": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@abacritt/angularx-social-login/-/angularx-social-login-2.1.0.tgz",
|
||||
"integrity": "sha512-wjKiwLhA0j/exrb17unRveAkjxYTf1qRYKXZqJfD4jSzHkODKjVpOVDmt7Q+T2m8fLJBGc304hRBuUVVEtl8xg==",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
|
|
@ -3360,6 +3349,12 @@
|
|||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/google.accounts": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.9.tgz",
|
||||
"integrity": "sha512-brOjhe+gTlZ241FMKcLjKp44CRSwdXNe8cWvHg23FBXEop1QRNhK9Gzs438dbCwCnZ0cCWDkQaaMBBYsZatZJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz",
|
||||
|
|
@ -7639,6 +7634,11 @@
|
|||
"node >= 0.2.0"
|
||||
]
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"node_modules/karma": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@abacritt/angularx-social-login": "^2.1.0",
|
||||
"@angular/animations": "^16.2.0",
|
||||
"@angular/common": "^16.2.0",
|
||||
"@angular/compiler": "^16.2.0",
|
||||
|
|
@ -19,6 +18,7 @@
|
|||
"@angular/platform-browser": "^16.2.0",
|
||||
"@angular/platform-browser-dynamic": "^16.2.0",
|
||||
"@angular/router": "^16.2.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.0"
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"@angular-devkit/build-angular": "^16.2.2",
|
||||
"@angular/cli": "~16.2.2",
|
||||
"@angular/compiler-cli": "^16.2.0",
|
||||
"@types/google.accounts": "^0.0.9",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"jasmine-core": "~4.6.0",
|
||||
|
|
@ -39,4 +40,4 @@
|
|||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
import {
|
||||
GoogleLoginProvider,
|
||||
SocialAuthService,
|
||||
} from '@abacritt/angularx-social-login';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
client/src/assets/google-color.svg
Normal file
28
client/src/assets/google-color.svg
Normal 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 |
0
client/src/assets/google-color.svg:Zone.Identifier
Normal file
0
client/src/assets/google-color.svg:Zone.Identifier
Normal file
BIN
client/src/assets/logo.png
Normal file
BIN
client/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
4
client/src/environments/environment.development.ts
Normal file
4
client/src/environments/environment.development.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const environment = {
|
||||
production: false,
|
||||
api: 'http://localhost:8080',
|
||||
};
|
||||
4
client/src/environments/environment.ts
Normal file
4
client/src/environments/environment.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
api: '',
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": [
|
||||
"google.accounts"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
|
|
@ -11,4 +13,4 @@
|
|||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -30,4 +30,4 @@
|
|||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
"jasmine",
|
||||
"google.accounts"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
server/.gitignore
vendored
3
server/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/cofffeemachineConfig
|
||||
/cofffeemachineConfig
|
||||
client_secret.json
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ module recipe-manager
|
|||
|
||||
go 1.21.1
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.0.10
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
)
|
||||
|
||||
require github.com/go-chi/cors v1.2.1 // indirect
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,3 +2,27 @@ github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
|||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
s := NewServer(3000)
|
||||
s := NewServer(8080)
|
||||
|
||||
serverCtx, serverStopCtx := context.WithCancel(context.Background())
|
||||
sig := make(chan os.Signal, 1)
|
||||
|
|
|
|||
198
server/routers/auth.go
Normal file
198
server/routers/auth.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package routers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type AuthRouter struct {
|
||||
gConfig *oauth2.Config
|
||||
nonce map[string]map[string]string
|
||||
}
|
||||
|
||||
func NewAuthRouter() *AuthRouter {
|
||||
|
||||
file, err := os.Open("client_secret.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var clientSecret map[string]interface{}
|
||||
json.NewDecoder(file).Decode(&clientSecret)
|
||||
|
||||
return &AuthRouter{
|
||||
gConfig: &oauth2.Config{
|
||||
ClientID: clientSecret["web"].(map[string]interface{})["client_id"].(string),
|
||||
ClientSecret: clientSecret["web"].(map[string]interface{})["client_secret"].(string),
|
||||
RedirectURL: "http://localhost:8080/auth/google/callback",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: clientSecret["web"].(map[string]interface{})["auth_uri"].(string),
|
||||
TokenURL: clientSecret["web"].(map[string]interface{})["token_uri"].(string),
|
||||
},
|
||||
},
|
||||
nonce: make(map[string]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (ar *AuthRouter) Route(r chi.Router) {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/google", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// generate state and nonce
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
state := base64.URLEncoding.EncodeToString(bytes)
|
||||
|
||||
stateMap := map[string]string{}
|
||||
|
||||
if r.URL.Query().Get("redirect_to") != "" {
|
||||
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
|
||||
}
|
||||
|
||||
ar.nonce[state] = stateMap
|
||||
|
||||
url := ar.gConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", "forth.co.th"), oauth2.SetAuthURLParam("include_granted_scopes", "true"), oauth2.AccessTypeOffline)
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/google/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
// check state
|
||||
|
||||
var redirect_to string
|
||||
if r.URL.Query().Get("state") == "" {
|
||||
http.Error(w, "State not found", http.StatusBadRequest)
|
||||
return
|
||||
} else {
|
||||
val, ok := ar.nonce[r.URL.Query().Get("state")]
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "Invalid state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
redirect_to = val["redirect_to"]
|
||||
|
||||
// remove state from nonce
|
||||
delete(ar.nonce, r.URL.Query().Get("state"))
|
||||
}
|
||||
|
||||
// exchange code for token
|
||||
token, err := ar.gConfig.Exchange(r.Context(), r.FormValue("code"))
|
||||
if err != nil {
|
||||
http.Error(w, "Error exchanging code for token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get user info
|
||||
client := ar.gConfig.Client(r.Context(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
|
||||
if err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&userInfo)
|
||||
|
||||
value := url.Values{}
|
||||
|
||||
value.Add("email", userInfo["email"].(string))
|
||||
value.Add("name", userInfo["name"].(string))
|
||||
value.Add("picture", userInfo["picture"].(string))
|
||||
if redirect_to != "" {
|
||||
value.Add("redirect_to", redirect_to)
|
||||
}
|
||||
|
||||
// redirect to frontend with token and refresh token
|
||||
w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
||||
w.Header().Add("set-cookie", "refresh_token="+token.RefreshToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
||||
http.Redirect(w, r, "http://localhost:4200/callback?"+value.Encode(), http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
// get refresh token from query string
|
||||
refreshToken := r.URL.Query().Get("refresh_token")
|
||||
redirectTo := r.URL.Query().Get("redirect_to")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "Refresh token not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// exchange refresh token for new token
|
||||
token, err := ar.gConfig.TokenSource(r.Context(), &oauth2.Token{RefreshToken: refreshToken}).Token()
|
||||
if err != nil {
|
||||
http.Error(w, "Error exchanging refresh token for token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to frontend with token and refresh token
|
||||
http.Redirect(w, r, "http://localhost:4200/callback?token="+token.AccessToken+"&redirect_to="+redirectTo, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/revoke", func(w http.ResponseWriter, r *http.Request) {
|
||||
// get access token and refresh token from cookie
|
||||
if cookie, err := r.Cookie("access_token"); err == nil {
|
||||
// request to revoke token at oauth2.googleapis.com/revoke
|
||||
_, err := http.PostForm("https://oauth2.googleapis.com/revoke", url.Values{"token": {cookie.Value}})
|
||||
if err != nil {
|
||||
http.Error(w, "Error revoking token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// remove cookie with expire from frontend and response no content
|
||||
w.Header().Add("set-cookie", "access_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0")
|
||||
w.Header().Add("set-cookie", "refresh_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
r.Get("/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
token := &oauth2.Token{}
|
||||
if cookie, err := r.Cookie("access_token"); err == nil {
|
||||
token.AccessToken = cookie.Value
|
||||
}
|
||||
|
||||
// if have refresh token, set refresh token to token
|
||||
if cookie, err := r.Cookie("refresh_token"); err == nil {
|
||||
token.RefreshToken = cookie.Value
|
||||
}
|
||||
|
||||
// get user info
|
||||
client := ar.gConfig.Client(r.Context(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
|
||||
if err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo map[string]interface{}
|
||||
if err = json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// return user info
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"user": map[string]string{
|
||||
"email": userInfo["email"].(string),
|
||||
"username": userInfo["name"].(string),
|
||||
"image": userInfo["picture"].(string),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ func NewRecipeRouter(data *data.Data) *RecipeRouter {
|
|||
}
|
||||
}
|
||||
|
||||
func (rr *RecipeRouter) Route(r *chi.Mux) {
|
||||
func (rr *RecipeRouter) Route(r chi.Router) {
|
||||
r.Route("/recipes", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
|
|
|||
131
server/server.go
131
server/server.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"recipe-manager/data"
|
||||
|
|
@ -36,54 +37,100 @@ func (s *Server) Run() error {
|
|||
|
||||
func createHandler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
AllowedOrigins: []string{"http://localhost:4200"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
ExposedHeaders: []string{"Content-Type"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
// Debug: true,
|
||||
}))
|
||||
r.Post("/merge", func(w http.ResponseWriter, r *http.Request) {
|
||||
var targetMap map[string]interface{}
|
||||
err := json.NewDecoder(r.Body).Decode(&targetMap)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed: ", err)
|
||||
return
|
||||
}
|
||||
log.Println(targetMap)
|
||||
master_version := targetMap["master"].(string)
|
||||
dev_version := targetMap["dev"].(string)
|
||||
|
||||
// find target file in the cofffeemachineConfig
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + master_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Master file not found: ", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + dev_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Dev file not found: ", err)
|
||||
return
|
||||
}
|
||||
database := data.NewData()
|
||||
|
||||
repo_path := "coffeemachineConfig/coffeethai02_"
|
||||
master_path := repo_path + master_version + ".json"
|
||||
dev_path := repo_path + dev_version + ".json"
|
||||
|
||||
// output path
|
||||
output_path := ""
|
||||
|
||||
// changelog path
|
||||
changelog_path := ""
|
||||
|
||||
// TODO: Call merge api if found
|
||||
err = exec.Command("python", "merge", master_path, dev_path, output_path, changelog_path).Run()
|
||||
if err != nil {
|
||||
log.Fatalln("Merge request failed. Python merge failed: ", err)
|
||||
}
|
||||
// Auth Router
|
||||
r.Group(func(r chi.Router) {
|
||||
ar := routers.NewAuthRouter()
|
||||
ar.Route(r)
|
||||
})
|
||||
|
||||
// Protect Group
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cookie, err := r.Cookie("access_token")
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// verify token by request to google api
|
||||
res, err := http.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + url.QueryEscape(cookie.Value))
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
var tokenInfo map[string]interface{}
|
||||
json.NewDecoder(res.Body).Decode(&tokenInfo)
|
||||
|
||||
log.Println(tokenInfo)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
// Recipe Router
|
||||
rr := routers.NewRecipeRouter(database)
|
||||
rr.Route(r)
|
||||
|
||||
r.Post("/merge", func(w http.ResponseWriter, r *http.Request) {
|
||||
var targetMap map[string]interface{}
|
||||
err := json.NewDecoder(r.Body).Decode(&targetMap)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed: ", err)
|
||||
return
|
||||
}
|
||||
log.Println(targetMap)
|
||||
master_version := targetMap["master"].(string)
|
||||
dev_version := targetMap["dev"].(string)
|
||||
|
||||
// find target file in the cofffeemachineConfig
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + master_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Master file not found: ", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + dev_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Dev file not found: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
repo_path := "coffeemachineConfig/coffeethai02_"
|
||||
master_path := repo_path + master_version + ".json"
|
||||
dev_path := repo_path + dev_version + ".json"
|
||||
|
||||
// output path
|
||||
output_path := ""
|
||||
|
||||
// changelog path
|
||||
changelog_path := ""
|
||||
|
||||
// TODO: Call merge api if found
|
||||
err = exec.Command("python", "merge", master_path, dev_path, output_path, changelog_path).Run()
|
||||
if err != nil {
|
||||
log.Fatalln("Merge request failed. Python merge failed: ", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
rr := routers.NewRecipeRouter(data.NewData())
|
||||
rr.Route(r)
|
||||
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue