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 @@
+
+
+
+

+
+ 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 @@
-
-
-
-

-
- 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);
- });
- }
-}