diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 933e92d..05bba1f 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -1,11 +1,62 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { NgModule, inject } from '@angular/core'; +import { CanActivateFn, Router, RouterModule, Routes } from '@angular/router'; +import { UserService } from './core/services/user.service'; +import { map } from 'rxjs'; + +const authGuard: CanActivateFn = () => { + const userService: UserService = inject(UserService); + const router: Router = inject(Router); + return userService.isAuthenticated.pipe( + map((isAuth) => isAuth || router.parseUrl('/login')) + ); +}; + +const loginGuard: CanActivateFn = () => { + const userService: UserService = inject(UserService); + const router: Router = inject(Router); + + return userService.isAuthenticated.pipe( + map((isAuth) => { + if (!isAuth) { + return true; + } + return router.parseUrl('/dashboard'); + }) + ); +}; const routes: Routes = [ + { + path: 'login', + loadComponent: () => + import('./core/auth/auth.component').then((m) => m.AuthComponent), + canActivate: [loginGuard], + }, + { + path: 'register', + loadComponent: () => + import('./core/auth/auth.component').then((m) => m.AuthComponent), + canActivate: [loginGuard], + }, { path: '', loadComponent: () => - import('./features/home/home.component').then((m) => m.HomeComponent), + import('./core/layout/layout.component').then((m) => m.LayoutComponent), + children: [ + { + path: '', + pathMatch: 'full', + redirectTo: 'dashboard', + }, + { + path: 'dashboard', + loadComponent: () => + import('./features/dashboard/dashboard.component').then( + (m) => m.DashboardComponent + ), + canActivate: [authGuard], + }, + ], }, ]; diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 51d2dd0..0680b43 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -1,5 +1 @@ - - - - diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 3b92e7e..b0f866a 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -2,16 +2,15 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; import { GoogleLoginProvider, - GoogleSigninButtonModule, SocialAuthServiceConfig, SocialLoginModule, } from '@abacritt/angularx-social-login'; -import { CoreModule } from './core/core.module'; import { FooterComponent } from './core/layout/footer.component'; import { HeaderComponent } from './core/layout/header.component'; +import { HttpClientModule } from '@angular/common/http'; +import { AppComponent } from './app.component'; @NgModule({ declarations: [AppComponent], @@ -21,6 +20,7 @@ import { HeaderComponent } from './core/layout/header.component'; HeaderComponent, AppRoutingModule, SocialLoginModule, + HttpClientModule, ], providers: [ { diff --git a/client/src/app/core/auth/auth.component.html b/client/src/app/core/auth/auth.component.html index e69de29..bbfa28a 100644 --- a/client/src/app/core/auth/auth.component.html +++ b/client/src/app/core/auth/auth.component.html @@ -0,0 +1,84 @@ +
+
+
+ Your Company +

+ Sign in to your account +

+
+ +
+
+
+
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+
diff --git a/client/src/app/core/auth/auth.component.ts b/client/src/app/core/auth/auth.component.ts index fea10e5..771392b 100644 --- a/client/src/app/core/auth/auth.component.ts +++ b/client/src/app/core/auth/auth.component.ts @@ -1,7 +1,15 @@ import { NgIf } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; -import { Form, FormControl, FormGroup } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { Errors } from '../models/errors.model'; +import { UserService } from '../services/user.service'; +import { + GoogleSigninButtonModule, + SocialAuthService, +} from '@abacritt/angularx-social-login'; +import { User } from '../models/user.model'; interface AuthForm { email: FormControl; @@ -12,30 +20,95 @@ interface AuthForm { @Component({ selector: 'app-auth-page', templateUrl: './auth.component.html', - imports: [RouterLink, NgIf], + imports: [RouterLink, NgIf, GoogleSigninButtonModule], standalone: true, }) -export class AuthComponent implements OnInit { +export class AuthComponent implements OnInit, OnDestroy { authType = ''; title = ''; + errors: Errors = { errors: {} }; isSubmitting = false; authForm: FormGroup; + destroy$ = new Subject(); constructor( private readonly route: ActivatedRoute, private readonly router: Router, - private readonly userService: UserService + private readonly userService: UserService, + private readonly authService: SocialAuthService ) { - // use FormBuilder to create a form group this.authForm = new FormGroup({ email: new FormControl('', { - validators: [Validators.required], + validators: [Validators.required, Validators.email], nonNullable: true, }), password: new FormControl('', { - validators: [Validators.required], + validators: [Validators.required, Validators.minLength(8)], nonNullable: true, }), }); } + + ngOnInit(): void { + this.authType = this.route.snapshot.url.at(-1)!.path; + this.title = this.authType === 'login' ? 'Sign in' : 'Sign up'; + if (this.authType === 'resigter') { + this.authForm.addControl( + 'username', + new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }) + ); + } + + // google login + this.authService.authState.subscribe((user) => { + this.userService.setAuth({ + username: user.name, + email: user.email, + token: user.idToken, + image: user.photoUrl, + bio: '', + } satisfies User); + + void this.router.navigate(['/dashboard']); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + summitForm(): void { + this.isSubmitting = true; + this.errors = { errors: {} }; + + let observable; + if (this.authType === 'login') { + observable = this.userService.login( + this.authForm.value as { + email: string; + password: string; + } + ); + } else { + observable = this.userService.register( + this.authForm.value as { + email: string; + password: string; + username: string; + } + ); + } + + observable.pipe(takeUntil(this.destroy$)).subscribe({ + next: () => void this.router.navigate(['/login']), + error: (err: Errors) => { + this.errors = err; + this.isSubmitting = false; + }, + }); + } } diff --git a/client/src/app/core/layout/header.component.ts b/client/src/app/core/layout/header.component.ts index 34322a9..b33cca1 100644 --- a/client/src/app/core/layout/header.component.ts +++ b/client/src/app/core/layout/header.component.ts @@ -1,8 +1,12 @@ +import { NgIf } from '@angular/common'; import { Component } from '@angular/core'; @Component({ selector: 'app-layout-header', templateUrl: './header.component.html', + imports: [NgIf], standalone: true, }) -export class HeaderComponent {} +export class HeaderComponent { + constructor() {} +} diff --git a/client/src/app/core/layout/layout.component.html b/client/src/app/core/layout/layout.component.html new file mode 100644 index 0000000..51d2dd0 --- /dev/null +++ b/client/src/app/core/layout/layout.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/app/core/layout/layout.component.ts b/client/src/app/core/layout/layout.component.ts new file mode 100644 index 0000000..863d2b0 --- /dev/null +++ b/client/src/app/core/layout/layout.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { HeaderComponent } from './header.component'; +import { FooterComponent } from './footer.component'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-layout', + templateUrl: './layout.component.html', + standalone: true, + imports: [HeaderComponent, FooterComponent, RouterModule], +}) +export class LayoutComponent {} diff --git a/client/src/app/core/models/errors.model.ts b/client/src/app/core/models/errors.model.ts new file mode 100644 index 0000000..afb8467 --- /dev/null +++ b/client/src/app/core/models/errors.model.ts @@ -0,0 +1,3 @@ +export interface Errors { + errors: { [key: string]: string }; +} diff --git a/client/src/app/core/models/user.model.ts b/client/src/app/core/models/user.model.ts new file mode 100644 index 0000000..a5d57ef --- /dev/null +++ b/client/src/app/core/models/user.model.ts @@ -0,0 +1,7 @@ +export interface User { + email: string; + token: string; + username: string; + bio: string; + image: string; +} diff --git a/client/src/app/core/services/jwt.service.ts b/client/src/app/core/services/jwt.service.ts new file mode 100644 index 0000000..954def4 --- /dev/null +++ b/client/src/app/core/services/jwt.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class JwtService { + getToken(): string | null { + return window.localStorage['jwtToken']; + } + + saveToken(token: string): void { + window.localStorage['jwtToken'] = token; + } + + destroyToken(): void { + window.localStorage.removeItem('jwtToken'); + } +} diff --git a/client/src/app/core/services/user.service.ts b/client/src/app/core/services/user.service.ts new file mode 100644 index 0000000..96c9919 --- /dev/null +++ b/client/src/app/core/services/user.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + BehaviorSubject, + Observable, + distinctUntilChanged, + map, + tap, +} from 'rxjs'; +import { User } from '../models/user.model'; +import { Router } from '@angular/router'; +import { JwtService } from './jwt.service'; + +@Injectable({ providedIn: 'root' }) +export class UserService { + private currnetUserSubject = new BehaviorSubject(null); + public currentUser = this.currnetUserSubject + .asObservable() + .pipe(distinctUntilChanged()); + + public isAuthenticated = this.currentUser.pipe( + map((user) => { + return user !== null; + }) + ); + + constructor( + private readonly http: HttpClient, + private readonly router: Router, + private readonly jwtService: JwtService + ) {} + + login(credentials: { + email: string; + password: string; + }): Observable<{ user: User }> { + return this.http + .post<{ user: User }>('/api/users/login', credentials) + .pipe(tap(({ user }) => this.setAuth(user))); + } + + register(credentials: { + username: string; + email: string; + password: string; + }): Observable<{ user: User }> { + return this.http + .post<{ user: User }>('/api/users', { user: credentials }) + .pipe(tap(({ user }) => this.setAuth(user))); + } + + logout(): void { + this.purgeAuth(); + void this.router.navigate(['/login']); + } + + getCurrentUser(): Observable<{ user: User }> { + return this.http.get<{ user: User }>('/api/user').pipe( + tap({ + next: ({ user }) => this.setAuth(user), + error: () => this.purgeAuth(), + }) + ); + } + + update(user: Partial): Observable<{ user: User }> { + return this.http + .put<{ user: User }>('/api/user', { user }) + .pipe(tap(({ user }) => this.currnetUserSubject.next(user))); + } + + setAuth(user: User): void { + this.jwtService.saveToken(user.token); + void this.currnetUserSubject.next(user); + } + + purgeAuth(): void { + this.jwtService.destroyToken(); + this.currnetUserSubject.next(null); + } +} diff --git a/client/src/app/features/home/home.component.css b/client/src/app/features/dashboard/dashboard.component.css similarity index 100% rename from client/src/app/features/home/home.component.css rename to client/src/app/features/dashboard/dashboard.component.css diff --git a/client/src/app/features/dashboard/dashboard.component.html b/client/src/app/features/dashboard/dashboard.component.html new file mode 100644 index 0000000..1ce6c22 --- /dev/null +++ b/client/src/app/features/dashboard/dashboard.component.html @@ -0,0 +1,4 @@ +

dashboard works!

+ diff --git a/client/src/app/features/dashboard/dashboard.component.ts b/client/src/app/features/dashboard/dashboard.component.ts new file mode 100644 index 0000000..2892038 --- /dev/null +++ b/client/src/app/features/dashboard/dashboard.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { UserService } from 'src/app/core/services/user.service'; +import { SocialAuthService } from '@abacritt/angularx-social-login'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-dashboard', + standalone: true, + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.css'], +}) +export class DashboardComponent { + constructor(private _userService: UserService) {} + + logout() { + console.log('logout'); + this._userService.logout(); + // this._authService.signOut(); + } +} diff --git a/client/src/app/features/home/home.component.html b/client/src/app/features/home/home.component.html deleted file mode 100644 index 1aa021e..0000000 --- a/client/src/app/features/home/home.component.html +++ /dev/null @@ -1,84 +0,0 @@ -
-
-
- Your Company -

- Sign in to your account -

-
- -
-
-
-
-
- -
- -
-
- -
-
- - -
-
- -
-
- -
- -
-
-
-
-
diff --git a/client/src/app/features/home/home.component.spec.ts b/client/src/app/features/home/home.component.spec.ts deleted file mode 100644 index ba1b4a3..0000000 --- a/client/src/app/features/home/home.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HomeComponent } from './home.component'; - -describe('HomeComponent', () => { - let component: HomeComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [HomeComponent] - }); - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/client/src/app/features/home/home.component.ts b/client/src/app/features/home/home.component.ts deleted file mode 100644 index 53ef21a..0000000 --- a/client/src/app/features/home/home.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - GoogleSigninButtonModule, - SocialAuthService, -} from '@abacritt/angularx-social-login'; -import { Component, OnInit } from '@angular/core'; -import { AppModule } from 'src/app/app.module'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.css'], - imports: [GoogleSigninButtonModule], - standalone: true, -}) -export class HomeComponent implements OnInit { - constructor(private _authService: SocialAuthService) {} - - ngOnInit(): void { - this._authService.authState.subscribe((user) => { - console.log(user); - }); - } -}