Dr-Swopt 6 months ago
parent
commit
61c9843e7f

+ 7 - 0
package-lock.json

@@ -15,6 +15,7 @@
         "@angular/platform-browser": "^19.2.0",
         "@angular/platform-browser-dynamic": "^19.2.0",
         "@angular/router": "^19.2.0",
+        "@simplewebauthn/browser": "^13.1.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.0"
@@ -4956,6 +4957,12 @@
         "node": "^18.17.0 || >=20.5.0"
       }
     },
+    "node_modules/@simplewebauthn/browser": {
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
+      "integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==",
+      "license": "MIT"
+    },
     "node_modules/@sindresorhus/merge-streams": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "@angular/platform-browser": "^19.2.0",
     "@angular/platform-browser-dynamic": "^19.2.0",
     "@angular/router": "^19.2.0",
+    "@simplewebauthn/browser": "^13.1.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"

File diff suppressed because it is too large
+ 0 - 191
src/app/app.component.html


+ 5 - 6
src/app/app.component.ts

@@ -1,12 +1,11 @@
 import { Component } from '@angular/core';
 import { RouterOutlet } from '@angular/router';
 
+// app.component.ts
 @Component({
   selector: 'app-root',
-  imports: [RouterOutlet],
-  templateUrl: './app.component.html',
-  styleUrl: './app.component.css'
+  standalone: true,
+  imports: [RouterOutlet], // shows routed components
+  template: `<router-outlet></router-outlet>`
 })
-export class AppComponent {
-  title = 'MobileAuthWebApp';
-}
+export class AppComponent {}

+ 11 - 1
src/app/app.routes.ts

@@ -1,3 +1,13 @@
+// app.routes.ts
 import { Routes } from '@angular/router';
+import { LoginComponent } from './login/login.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
+import { authGuard } from './services/auth.guard';
+import { RegisterComponent } from './register/register.component';
 
-export const routes: Routes = [];
+export const routes: Routes = [
+  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
+  { path: 'login', component: LoginComponent },
+  { path: 'register', component: RegisterComponent },
+  { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
+];

+ 0 - 0
src/app/dashboard/dashboard.component.css


+ 7 - 0
src/app/dashboard/dashboard.component.html

@@ -0,0 +1,7 @@
+<h2>Welcome to the dashboard!</h2>
+<!-- Make sure you see this line first to confirm the page is rendering -->
+<p>🚨 Dashboard page is working</p>
+<p>Server URL: {{ serverUrl }}</p>
+
+<!-- Now the logout button -->
+<button (click)="logout()">Logout</button>

+ 23 - 0
src/app/dashboard/dashboard.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DashboardComponent } from './dashboard.component';
+
+describe('DashboardComponent', () => {
+  let component: DashboardComponent;
+  let fixture: ComponentFixture<DashboardComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [DashboardComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(DashboardComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 28 - 0
src/app/dashboard/dashboard.component.ts

@@ -0,0 +1,28 @@
+import { Component, OnInit } from '@angular/core';
+import { AuthService } from '../services/auth.service';
+
+@Component({
+  selector: 'app-dashboard',
+  standalone: true,
+  templateUrl: './dashboard.component.html'
+})
+export class DashboardComponent implements OnInit {
+  serverUrl: string | null = null;
+
+  constructor(private auth: AuthService) {}
+
+  ngOnInit(): void {
+    this.auth.getServerUrl().subscribe({
+      next: (url) => {
+        this.serverUrl = url;
+      },
+      error: (err) => {
+        console.error('Error fetching server URL:', err);
+      }
+    });
+  }
+
+  logout() {
+    this.auth.logout();
+  }
+}

+ 15 - 0
src/app/interfaces/interface.ts

@@ -0,0 +1,15 @@
+// Define clear types for input and output
+interface RegisterPayload {
+  name: string;
+  email: string;
+  password: string;
+}
+
+interface LoginPayload {
+  email: string;
+  password: string;
+}
+
+interface AuthResponse {
+  token: string;
+}

+ 0 - 0
src/app/login/login.component.css


+ 20 - 0
src/app/login/login.component.html

@@ -0,0 +1,20 @@
+<form [formGroup]="form" (ngSubmit)="onSubmit()">
+  <h2>Login</h2>
+
+  <input formControlName="email" type="email" placeholder="Email" />
+  <div *ngIf="form.get('email')?.invalid && form.get('email')?.touched" class="error">
+    Valid email is required.
+  </div>
+
+  <input formControlName="password" type="password" placeholder="Password" />
+  <div *ngIf="form.get('password')?.invalid && form.get('password')?.touched" class="error">
+    Password is required.
+  </div>
+
+  <button type="submit">Login</button>
+
+  <div style="margin-top: 1rem;">
+    <p>Don't have an account?</p>
+    <button type="button" (click)="goToRegister()">Register</button>
+  </div>
+</form>

+ 23 - 0
src/app/login/login.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [LoginComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 51 - 0
src/app/login/login.component.ts

@@ -0,0 +1,51 @@
+import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { Router, RouterModule } from '@angular/router';
+import { AuthService } from '../services/auth.service';
+
+@Component({
+  selector: 'app-login',
+  standalone: true,
+  imports: [CommonModule, ReactiveFormsModule, RouterModule],
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+  form!: FormGroup;
+
+  constructor(
+    private fb: FormBuilder,
+    private auth: AuthService,
+    private router: Router
+  ) {}
+
+  ngOnInit(): void {
+    this.form = this.fb.group({
+      email: ['', [Validators.required, Validators.email]],
+      password: ['', Validators.required]
+    });
+  }
+
+  goToRegister(): void {
+    this.router.navigate(['/register']);
+  }
+
+  onSubmit(): void {
+    if (this.form.invalid) {
+      this.form.markAllAsTouched();
+      return;
+    }
+
+    this.auth.login(this.form.value).subscribe({
+      next: (res) => {
+        this.auth.storeToken(res.token);
+        this.router.navigate(['/dashboard']);
+      },
+      error: (err) => {
+        console.error('Login failed', err);
+        alert('Login failed: ' + (err.error?.message || 'Unknown error'));
+      }
+    });
+  }
+}

+ 0 - 0
src/app/register/register.component.css


+ 9 - 0
src/app/register/register.component.html

@@ -0,0 +1,9 @@
+<form [formGroup]="form" (ngSubmit)="onSubmit()">
+  <h2>Register</h2>
+
+  <input formControlName="name" placeholder="Name" />
+  <input formControlName="email" placeholder="Email" />
+  <input formControlName="password" placeholder="Password" type="password" />
+
+  <button type="submit">Register</button>
+</form>

+ 23 - 0
src/app/register/register.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RegisterComponent } from './register.component';
+
+describe('RegisterComponent', () => {
+  let component: RegisterComponent;
+  let fixture: ComponentFixture<RegisterComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [RegisterComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(RegisterComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 49 - 0
src/app/register/register.component.ts

@@ -0,0 +1,49 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import { AuthService } from '../services/auth.service';
+
+@Component({
+  standalone: true,
+  selector: 'app-register',
+  imports: [CommonModule, ReactiveFormsModule],
+  templateUrl: './register.component.html'
+})
+export class RegisterComponent implements OnInit {
+  form!: FormGroup;
+
+  constructor(
+    private fb: FormBuilder,
+    private router: Router,
+    private auth: AuthService
+  ) {}
+
+  ngOnInit(): void {
+    this.form = this.fb.group({
+      name: ['', Validators.required],
+      email: ['', [Validators.required, Validators.email]],
+      password: ['', [Validators.required, Validators.minLength(6)]]
+    });
+  }
+
+  onSubmit(): void {
+    if (this.form.invalid) {
+      this.form.markAllAsTouched();
+      return;
+    }
+
+    const formValue = this.form.value;
+
+    this.auth.register(formValue).subscribe({
+      next: (res) => {
+        this.auth.storeToken(res.token);
+        this.router.navigate(['/dashboard']);
+      },
+      error: (err) => {
+        console.error('Registration failed', err);
+        alert('Registration failed: ' + (err.error?.message || 'Unknown error'));
+      }
+    });
+  }
+}

+ 17 - 0
src/app/services/auth.guard.ts

@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+import { CanActivateFn } from '@angular/router';
+import { inject } from '@angular/core';
+import { Router } from '@angular/router';
+import { AuthService } from './auth.service';
+
+export const authGuard: CanActivateFn = () => {
+  const auth = inject(AuthService);
+  const router = inject(Router);
+
+  if (auth.isLoggedIn()) {
+    return true;
+  }
+
+  router.navigate(['/login']);
+  return false;
+};

+ 16 - 0
src/app/services/auth.interceptor.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+import { HttpInterceptorFn } from '@angular/common/http';
+
+
+describe('authInterceptor', () => {
+  const interceptor: HttpInterceptorFn = (req, next) => 
+    TestBed.runInInjectionContext(() => authInterceptor(req, next));
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+  });
+
+  it('should be created', () => {
+    expect(interceptor).toBeTruthy();
+  });
+});

+ 22 - 0
src/app/services/auth.interceptor.ts

@@ -0,0 +1,22 @@
+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 auth: AuthService) {}
+
+  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    const token = this.auth.getToken();
+    if (token) {
+      const authReq = req.clone({
+        setHeaders: {
+          Authorization: `Bearer ${token}`
+        }
+      });
+      return next.handle(authReq);
+    }
+    return next.handle(req);
+  }
+}

+ 16 - 0
src/app/services/auth.service.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AuthService } from './auth.service';
+
+describe('AuthService', () => {
+  let service: AuthService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(AuthService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});

+ 47 - 0
src/app/services/auth.service.ts

@@ -0,0 +1,47 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Router } from '@angular/router';
+import { Observable } from 'rxjs';
+
+@Injectable({ providedIn: 'root' })
+export class AuthService {
+  private readonly baseUrl = 'http://localhost:3000';
+  private readonly tokenKey = 'auth_token';
+
+  constructor(private http: HttpClient, private router: Router) { }
+
+  // -- API Calls --
+
+  register(payload: RegisterPayload): Observable<AuthResponse> {
+    return this.http.post<AuthResponse>(`${this.baseUrl}/auth/register`, payload);
+  }
+
+  login(payload: LoginPayload): Observable<AuthResponse> {
+    return this.http.post<AuthResponse>(`${this.baseUrl}/auth/login`, payload);
+  }
+
+  // -- Token Management --
+
+  storeToken(token: string): void {
+    localStorage.setItem(this.tokenKey, token);
+  }
+
+  getToken(): string | null {
+    return localStorage.getItem(this.tokenKey);
+  }
+
+  isLoggedIn(): boolean {
+    return !!this.getToken();
+  }
+
+  logout(): void {
+    localStorage.removeItem(this.tokenKey);
+    this.router.navigate(['/login']);
+  }
+
+  getServerUrl(): Observable<string> {
+    return this.http.get(`${this.baseUrl}/server`, {
+      responseType: 'text'
+    });
+  }
+}

+ 7 - 3
src/main.ts

@@ -1,6 +1,10 @@
 import { bootstrapApplication } from '@angular/platform-browser';
-import { appConfig } from './app/app.config';
 import { AppComponent } from './app/app.component';
+import { provideRouter } from '@angular/router';
+import { routes } from './app/app.routes';
+import { HTTP_INTERCEPTORS, provideHttpClient } from '@angular/common/http';
+import { AuthInterceptor } from './app/services/auth.interceptor';
 
-bootstrapApplication(AppComponent, appConfig)
-  .catch((err) => console.error(err));
+bootstrapApplication(AppComponent, {
+  providers: [provideRouter(routes), provideHttpClient(), { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }]
+});

Some files were not shown because too many files changed in this diff