From f2ddc38c5f41581fcdf4fa6c1c48b8dd125bc2c3 Mon Sep 17 00:00:00 2001 From: Keshav Singh Date: Sun, 7 Jan 2024 18:42:57 +0530 Subject: [PATCH] Added GITHUB AUTH --- package-lock.json | 19 +- package.json | 1 + src/app/app-routing.module.ts | 2 +- src/app/app.module.ts | 15 +- .../components/admin/admin-routing.module.ts | 4 +- src/app/components/admin/admin.module.ts | 2 +- .../admin/user/user/user.component.html | 236 +++++++++++++++++- .../admin/user/user/user.component.ts | 7 + .../github-callback.component.css | 0 .../github-callback.component.html | 1 + .../github-callback.component.spec.ts | 21 ++ .../github-callback.component.ts | 15 ++ .../oauth/login/login.component.html | 20 +- .../components/oauth/login/login.component.ts | 15 ++ .../oauth-button/oauth-button.component.html | 2 +- .../oauth-button/oauth-button.component.ts | 4 +- .../components/oauth/oauth-routing.module.ts | 2 + src/app/guards/auth/auth.service.ts | 90 ++++++- .../auth-interceptor.service.spec.ts | 16 ++ .../interceptor/auth-interceptor.service.ts | 31 +++ src/app/models/admin/navbar/menu.ts | 28 +-- src/environments/environment.ts | 4 + 22 files changed, 494 insertions(+), 41 deletions(-) create mode 100644 src/app/components/callback/github-callback/github-callback.component.css create mode 100644 src/app/components/callback/github-callback/github-callback.component.html create mode 100644 src/app/components/callback/github-callback/github-callback.component.spec.ts create mode 100644 src/app/components/callback/github-callback/github-callback.component.ts create mode 100644 src/app/guards/auth/interceptor/auth-interceptor.service.spec.ts create mode 100644 src/app/guards/auth/interceptor/auth-interceptor.service.ts diff --git a/package-lock.json b/package-lock.json index 425937b..4fd7c0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", "@popperjs/core": "^2.11.6", - "@types/youtube": "^0.0.50", + "angular-oauth2-oidc": "^17.0.1", "aos": "^2.3.4", "bootstrap": "^5.3.2", "crypto-js": "^4.2.0", @@ -6418,11 +6418,6 @@ "@types/node": "*" } }, - "node_modules/@types/youtube": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/youtube/-/youtube-0.0.50.tgz", - "integrity": "sha512-d4GpH4uPYp9W07kc487tiq6V/EUHl18vZWFMbQoe4Sk9LXEWzFi/BMf9x7TI4m7/j7gU3KeX8H6M8aPBgykeLw==" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -7139,6 +7134,18 @@ "ajv": "^8.8.2" } }, + "node_modules/angular-oauth2-oidc": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-17.0.1.tgz", + "integrity": "sha512-Yl4It9zFsYmoNS73sUvNJstbMW1x73ejKonzXLgU4XnSuBCt/0x8PnY5R3mHX4ZC/WmXBqQ/RfFwClrYW9Ywcg==", + "dependencies": { + "tslib": "^2.5.2" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/package.json b/package.json index ab89666..3126a9b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", "@popperjs/core": "^2.11.6", + "angular-oauth2-oidc": "^17.0.1", "aos": "^2.3.4", "bootstrap": "^5.3.2", "crypto-js": "^4.2.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index a502b9d..e125390 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import { SettingComponent } from './components/setting/setting.component'; import { authGuard } from './guards/auth/auth.guard'; import { HomeComponent } from './components/home/home.component'; import { NotFoundComponent } from './components/general/not-found/not-found.component'; +import { OauthButtonComponent } from './components/oauth/oauth-button/oauth-button.component'; const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, @@ -36,7 +37,6 @@ const routes: Routes = [ path: 'other', loadChildren: () => import('./components/other-activity/other-activity.module').then(m => m.OtherActivityModule) }, - { path: '404', component: NotFoundComponent }, { path: '**', redirectTo: '/404', pathMatch: 'full' } ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bfc5320..b510806 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; -import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; @@ -43,6 +43,8 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; import { QuestionEditDialogComponent } from './components/general/dialog/topic/question-edit-dialog/question-edit-dialog.component'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatChipsModule } from '@angular/material/chips'; +import { GithubCallbackComponent } from './components/callback/github-callback/github-callback.component'; +import { AuthInterceptor } from './guards/auth/interceptor/auth-interceptor.service'; // AOT compilation support export function HttpLoaderFactory(http: HttpClient) { @@ -76,7 +78,8 @@ export function HttpLoaderFactory(http: HttpClient) { ConfettiComponent, FileUploadComponent, ConfirmDialogComponent, - QuestionEditDialogComponent + QuestionEditDialogComponent, + GithubCallbackComponent ], // ... @@ -107,7 +110,13 @@ export function HttpLoaderFactory(http: HttpClient) { MatProgressBarModule, MatChipsModule // Add this line ], - providers: [], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + } + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/components/admin/admin-routing.module.ts b/src/app/components/admin/admin-routing.module.ts index bdf5484..9c64866 100644 --- a/src/app/components/admin/admin-routing.module.ts +++ b/src/app/components/admin/admin-routing.module.ts @@ -9,6 +9,7 @@ import { ChatComponent } from './chat/chat.component'; import { MarkdownRendererComponent } from './markdown-renderer/markdown-renderer.component'; import { FileUploadComponent } from '../general/file/file-upload/file-upload.component'; import { CsharpInterviewQaComponent } from './topic/csharp-interview-qa/csharp-interview-qa.component'; +import { UserComponent } from './user/user/user.component'; const routes: Routes = [{ path: "", component: AdminComponent, children: [ @@ -19,7 +20,8 @@ const routes: Routes = [{ { path: "chat", component: ChatComponent }, { path: "markdown-renderer", component: MarkdownRendererComponent }, { path: "file", component: FileUploadComponent }, - { path: "topic", component: CsharpInterviewQaComponent } + { path: "topic", component: CsharpInterviewQaComponent }, + { path: "user", component: UserComponent } ] }]; diff --git a/src/app/components/admin/admin.module.ts b/src/app/components/admin/admin.module.ts index d16b4b3..c241bd7 100644 --- a/src/app/components/admin/admin.module.ts +++ b/src/app/components/admin/admin.module.ts @@ -80,7 +80,7 @@ import { CsharpInterviewQaComponent } from './topic/csharp-interview-qa/csharp-i MatTableModule, MatDialogModule, MatSelectModule, - MatOptionModule + MatOptionModule, ] }) export class AdminModule { } diff --git a/src/app/components/admin/user/user/user.component.html b/src/app/components/admin/user/user/user.component.html index d039bb7..572de36 100644 --- a/src/app/components/admin/user/user/user.component.html +++ b/src/app/components/admin/user/user/user.component.html @@ -1 +1,235 @@ -

user works!

+
+
+
Your profile is 70% Complete
+
+
+
+
+ +

General Information

+ +
+
+
+ + +
+
+
+ Profile Picture +
+ + Change Profile Picture + +
+
+
+
+
+
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+
+
+ + +
+ +
+
+ +
+ +
+
+ +
+ + +
+ +
+
+
+
+ +

Change Password

+ +
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+
+
+ +

Contact Information

+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+ +

Social Profiles

+ + + +

Send Email Notifications

+ + + +
+
diff --git a/src/app/components/admin/user/user/user.component.ts b/src/app/components/admin/user/user/user.component.ts index 6f8bec9..01dd13c 100644 --- a/src/app/components/admin/user/user/user.component.ts +++ b/src/app/components/admin/user/user/user.component.ts @@ -6,5 +6,12 @@ import { Component } from '@angular/core'; styleUrls: ['./user.component.css'] }) export class UserComponent { +handleKeyDown() { +throw new Error('Method not implemented.'); +} +uploadPicture() { +throw new Error('Method not implemented.'); +} +picture: any; } diff --git a/src/app/components/callback/github-callback/github-callback.component.css b/src/app/components/callback/github-callback/github-callback.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/callback/github-callback/github-callback.component.html b/src/app/components/callback/github-callback/github-callback.component.html new file mode 100644 index 0000000..cbf5d37 --- /dev/null +++ b/src/app/components/callback/github-callback/github-callback.component.html @@ -0,0 +1 @@ +

Processing login...

diff --git a/src/app/components/callback/github-callback/github-callback.component.spec.ts b/src/app/components/callback/github-callback/github-callback.component.spec.ts new file mode 100644 index 0000000..fe317d0 --- /dev/null +++ b/src/app/components/callback/github-callback/github-callback.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GithubCallbackComponent } from './github-callback.component'; + +describe('GithubCallbackComponent', () => { + let component: GithubCallbackComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [GithubCallbackComponent] + }); + fixture = TestBed.createComponent(GithubCallbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/callback/github-callback/github-callback.component.ts b/src/app/components/callback/github-callback/github-callback.component.ts new file mode 100644 index 0000000..5685ad9 --- /dev/null +++ b/src/app/components/callback/github-callback/github-callback.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from 'src/app/guards/auth/auth.service'; + +@Component({ + selector: 'app-github-callback', + templateUrl: './github-callback.component.html', + styleUrls: ['./github-callback.component.css'] +}) +export class GithubCallbackComponent implements OnInit { + constructor(private authService: AuthService) { } + + ngOnInit() { + this.authService.handleAuthentication(); + } +} diff --git a/src/app/components/oauth/login/login.component.html b/src/app/components/oauth/login/login.component.html index 3c43c6a..3b2e130 100644 --- a/src/app/components/oauth/login/login.component.html +++ b/src/app/components/oauth/login/login.component.html @@ -47,11 +47,21 @@

Sign in


- - - - - + + + + + + + + +
diff --git a/src/app/components/oauth/login/login.component.ts b/src/app/components/oauth/login/login.component.ts index 30c2114..5d83e4e 100644 --- a/src/app/components/oauth/login/login.component.ts +++ b/src/app/components/oauth/login/login.component.ts @@ -82,4 +82,19 @@ export class LoginComponent implements OnInit { } ); } + + onLoginRequested(service: string): void { + console.log(`Login requested for: ${service}`); + switch (service.toLowerCase()) { + case 'github': + this.authService.initiateGithubLogin(); + break; + case 'linkedin': + // Call LinkedIn login method + break; + // Add more cases for other services + default: + console.warn(`Login for service ${service} is not implemented.`); + } + } } diff --git a/src/app/components/oauth/oauth-button/oauth-button.component.html b/src/app/components/oauth/oauth-button/oauth-button.component.html index 0c96ae8..7ff1d30 100644 --- a/src/app/components/oauth/oauth-button/oauth-button.component.html +++ b/src/app/components/oauth/oauth-button/oauth-button.component.html @@ -1,3 +1,3 @@ - + {{ service }} diff --git a/src/app/components/oauth/oauth-button/oauth-button.component.ts b/src/app/components/oauth/oauth-button/oauth-button.component.ts index e3ea7cd..cc88b95 100644 --- a/src/app/components/oauth/oauth-button/oauth-button.component.ts +++ b/src/app/components/oauth/oauth-button/oauth-button.component.ts @@ -13,12 +13,14 @@ import { pulse } from 'ng-animate'; ] }) export class OauthButtonComponent { + @Input() service = ''; @Input() iconClass = ''; @HostBinding('@pulse') pulse = true; @Output() loginRequested = new EventEmitter(); - public login(): void { + public login(event: Event): void { + event.preventDefault(); // Prevent default anchor behavior console.log('Login requested for ' + this.service); this.loginRequested.emit(this.service); } diff --git a/src/app/components/oauth/oauth-routing.module.ts b/src/app/components/oauth/oauth-routing.module.ts index 8f0e304..30b592c 100644 --- a/src/app/components/oauth/oauth-routing.module.ts +++ b/src/app/components/oauth/oauth-routing.module.ts @@ -7,6 +7,7 @@ import { LoginComponent } from './login/login.component'; import { SignupComponent } from './signup/signup.component'; import { LoginSuccessComponent } from './login-success/login-success.component'; import { LoginFailureComponent } from './login-failure/login-failure.component'; +import { GithubCallbackComponent } from '../callback/github-callback/github-callback.component'; const routes: Routes = [ { @@ -19,6 +20,7 @@ const routes: Routes = [ { path: "signup", component: SignupComponent }, { path: 'login-success', component: LoginSuccessComponent }, { path: 'login-failure', component: LoginFailureComponent }, + { path: 'github-callback', component: GithubCallbackComponent }, ] }]; diff --git a/src/app/guards/auth/auth.service.ts b/src/app/guards/auth/auth.service.ts index c5925f1..d1a960c 100644 --- a/src/app/guards/auth/auth.service.ts +++ b/src/app/guards/auth/auth.service.ts @@ -2,24 +2,49 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { UserDetails } from '../../models/user-details'; import { ProfileService } from '../../services/profile/profile.service'; +import { HttpClient } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; +import { OAuthService, AuthConfig, OAuthSuccessEvent } from 'angular-oauth2-oidc'; +import { ActivatedRoute, Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class AuthService { + // private isAuthenticated = new BehaviorSubject(false); + private authState = new BehaviorSubject(this.hasValidToken()); + private readonly tokenStorageKey = 'access_token'; - private isAuthenticated = new BehaviorSubject(false); - constructor(private profileService: ProfileService) { } + constructor(private profileService: ProfileService, private http: HttpClient, + private router: Router, + private route: ActivatedRoute) { + } + isAuthenticated() { + return this.authState.asObservable(); + } + saveAccessToken(token: string) { + localStorage.setItem(this.tokenStorageKey, token); + this.authState.next(true); + } + getAccessToken() { + return localStorage.getItem(this.tokenStorageKey); + } + hasValidToken(): boolean { + const token = this.getAccessToken(); + // Here you would check if the token is valid, e.g., not expired + // For simplicity, we assume a token exists means it's valid + return !!token; + } - login(organizationId: string, username: string, password: string): boolean { + public login(organizationId: string, username: string, password: string): boolean { // Here you should implement your authentication logic, e.g., check credentials against a backend // For this example, we're just setting isAuthenticated to true - this.isAuthenticated.next(true); + this.authState.next(true); console.log('user authenticated successfully!'); console.log(password); // Assuming getUserDetails returns an Observable, you need to subscribe to it to get the data - this.profileService.get(organizationId,username).subscribe((userDetails: UserDetails) => { + this.profileService.get(organizationId, username).subscribe((userDetails: UserDetails) => { // Store user data after retrieving it from the profile service this.storeUserData('some-token', userDetails); // Replace 'some-token' with the actual token }); @@ -30,8 +55,10 @@ export class AuthService { logout(): void { - this.isAuthenticated.next(false); + // this.isAuthenticated.next(false); + this.authState.next(false); + localStorage.removeItem(this.tokenStorageKey); localStorage.removeItem('token'); localStorage.removeItem('currentUserName'); localStorage.removeItem('organizationId'); @@ -40,7 +67,8 @@ export class AuthService { } isLoggedIn(): Observable { - return this.isAuthenticated.asObservable(); + return this.isAuthenticated(); + // return this.isAuthenticated.asObservable(); } storeUserData(token: string, user: UserDetails) { @@ -65,4 +93,52 @@ export class AuthService { const oauthUrl = `/auth/${provider.toLowerCase()}`; window.location.href = oauthUrl; } + + public handleAuthentication(): void { + // Get the code from the URL query parameters + this.route.queryParams.subscribe(params => { + const code = params['code']; + + if (code) { + this.sendCodeToBackend(code); + } else { + // Handle the case where there is no code in the query parameters + } + }); + } + private sendCodeToBackend(code: string): void { + const apiUrl = `${environment.awsUserApiBaseUrl}`; + this.http.post(`${apiUrl}/github/callback`, { code }).subscribe( + response => { + // The backend should return an object containing the access token or the session info + // Save the session info as needed + // Redirect or perform actions as needed after successful login + console.log('response from github callback:'); + console.log(response); + + this.router.navigate(['/index']); // Redirect to dashboard or other component + }, + err => { + console.error('Error exchanging code for token:', err); + // Handle the error + } + ); + } + + public initiateGithubLogin(): void { + const clientId = environment.github.clientId; + const redirectUri = environment.github.redirectUri; + const scope = 'read:user'; // Adjust the scope according to your needs + const state = this.generateRandomString(); // A random string to prevent CSRF attacks + + const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`; + + window.location.href = githubAuthUrl; + } + private generateRandomString(): string { + const array = new Uint32Array(10); + window.crypto.getRandomValues(array); + return array.join(''); + } + } diff --git a/src/app/guards/auth/interceptor/auth-interceptor.service.spec.ts b/src/app/guards/auth/interceptor/auth-interceptor.service.spec.ts new file mode 100644 index 0000000..03e16c3 --- /dev/null +++ b/src/app/guards/auth/interceptor/auth-interceptor.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthInterceptorService } from './auth-interceptor.service'; + +describe('AuthInterceptorService', () => { + let service: AuthInterceptorService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthInterceptorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/guards/auth/interceptor/auth-interceptor.service.ts b/src/app/guards/auth/interceptor/auth-interceptor.service.ts new file mode 100644 index 0000000..a34d7c5 --- /dev/null +++ b/src/app/guards/auth/interceptor/auth-interceptor.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { + HttpInterceptor, + HttpRequest, + HttpHandler, + HttpEvent, +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AuthService } from '../auth.service'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(private authService: AuthService) { } + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + const accessToken = this.authService.getAccessToken(); + if (accessToken) { + const clonedReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return next.handle(clonedReq); + } else { + return next.handle(req); + } + } +} diff --git a/src/app/models/admin/navbar/menu.ts b/src/app/models/admin/navbar/menu.ts index 2d08470..4e06c05 100644 --- a/src/app/models/admin/navbar/menu.ts +++ b/src/app/models/admin/navbar/menu.ts @@ -10,25 +10,25 @@ export const menu: NavItem[] = [ displayName: 'User', iconName: 'face', route: 'user', - children: [ - { - displayName: 'Account Info', - iconName: 'account_box', - route: 'user/account-info' - } - ] + // children: [ + // { + // displayName: 'Account Info', + // iconName: 'account_box', + // route: 'user/account-info' + // } + // ] }, { displayName: 'Organization', iconName: 'business', route: 'organization', - children: [ - { - displayName: 'Account Info', - iconName: 'account_box', - route: '/admin/organization' - } - ] + // children: [ + // { + // displayName: 'Account Info', + // iconName: 'account_box', + // route: '/admin/organization' + // } + // ] }, { displayName: 'Visitor', diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a0a5fa2..b184b28 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -68,5 +68,9 @@ export const environment = { fileApiEndpoints: { getUrl:'/api/file/geturl/{key}', generateUrl:'/api/file/generateUrl/{key}', + }, + github:{ + clientId: '26cb4ea080bd30fe7461', + redirectUri: 'http://localhost:4200/authentication/github-callback', } };