diff --git a/angular.json b/angular.json
index 8999310..c94fd04 100644
--- a/angular.json
+++ b/angular.json
@@ -47,7 +47,13 @@
"development": {
"optimization": false,
"extractLicenses": false,
- "sourceMap": true
+ "sourceMap": true,
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.development.ts"
+ }
+ ]
}
},
"defaultConfiguration": "production"
diff --git a/package-lock.json b/package-lock.json
index 723f5fa..7db7a8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@angular/forms": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
+ "bootstrap": "^5.3.8",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
@@ -3240,6 +3241,17 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.47",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz",
@@ -4630,6 +4642,25 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
diff --git a/package.json b/package.json
index 1bf77d0..5dc1b2f 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@angular/forms": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
+ "bootstrap": "^5.3.8",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
@@ -43,4 +44,4 @@
"typescript": "~5.9.2",
"vitest": "^4.0.8"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/app.html b/src/app/app.html
index e0118a1..67e7bd4 100644
--- a/src/app/app.html
+++ b/src/app/app.html
@@ -1,342 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Hello, {{ title() }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index dc39edb..18fc308 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,3 +1,25 @@
import { Routes } from '@angular/router';
+import { Login } from '@pages/auth/login/login';
+import { Signup } from '@pages/auth/signup/signup';
+import { AppLayout } from '@layouts/app-layout/app-layout';
+import { authGuard } from '@guards/auth-guard';
-export const routes: Routes = [];
+export const routes: Routes = [
+ {
+ path: '',
+ component: AppLayout,
+ canActivate: [authGuard]
+ },
+{
+ path: 'login',
+ component: Login,
+ },
+ {
+ path: 'sign-up',
+ component: Signup,
+ },
+ {
+ path: '**',
+ redirectTo: '/',
+ },
+];
diff --git a/src/app/guards/auth-guard.spec.ts b/src/app/guards/auth-guard.spec.ts
new file mode 100644
index 0000000..0ffeaf0
--- /dev/null
+++ b/src/app/guards/auth-guard.spec.ts
@@ -0,0 +1,17 @@
+import { TestBed } from '@angular/core/testing';
+import { CanActivateFn } from '@angular/router';
+
+import { authGuard } from './auth-guard';
+
+describe('authGuard', () => {
+ const executeGuard: CanActivateFn = (...guardParameters) =>
+ TestBed.runInInjectionContext(() => authGuard(...guardParameters));
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ });
+
+ it('should be created', () => {
+ expect(executeGuard).toBeTruthy();
+ });
+});
diff --git a/src/app/guards/auth-guard.ts b/src/app/guards/auth-guard.ts
new file mode 100644
index 0000000..a798f2c
--- /dev/null
+++ b/src/app/guards/auth-guard.ts
@@ -0,0 +1,14 @@
+import { inject } from '@angular/core';
+import { CanActivateFn, Router } from '@angular/router';
+import { Authorization } from '@services/authorization/authorization';
+
+export const authGuard: CanActivateFn = (route, state) => {
+ const authService = inject(Authorization);
+ const router = inject(Router);
+ if (authService.isLoggedIn()) {
+ return true; // Allow access
+ } else {
+ router.navigate(['/login']); // Redirect to login
+ return false; // Deny access
+ }
+};
diff --git a/src/app/layouts/app-layout/app-layout.css b/src/app/layouts/app-layout/app-layout.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/layouts/app-layout/app-layout.html b/src/app/layouts/app-layout/app-layout.html
new file mode 100644
index 0000000..f08ea58
--- /dev/null
+++ b/src/app/layouts/app-layout/app-layout.html
@@ -0,0 +1 @@
+
app-layout works!
diff --git a/src/app/layouts/app-layout/app-layout.spec.ts b/src/app/layouts/app-layout/app-layout.spec.ts
new file mode 100644
index 0000000..93ba684
--- /dev/null
+++ b/src/app/layouts/app-layout/app-layout.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppLayout } from './app-layout';
+
+describe('AppLayout', () => {
+ let component: AppLayout;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AppLayout]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AppLayout);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/layouts/app-layout/app-layout.ts b/src/app/layouts/app-layout/app-layout.ts
new file mode 100644
index 0000000..50d5c15
--- /dev/null
+++ b/src/app/layouts/app-layout/app-layout.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-app-layout',
+ imports: [],
+ templateUrl: './app-layout.html',
+ styleUrl: './app-layout.css',
+})
+export class AppLayout {
+
+}
diff --git a/src/app/models/user.ts b/src/app/models/user.ts
new file mode 100644
index 0000000..636915a
--- /dev/null
+++ b/src/app/models/user.ts
@@ -0,0 +1,4 @@
+export type User = {
+ id: number;
+ email: string;
+};
\ No newline at end of file
diff --git a/src/app/pages/auth/login/login.css b/src/app/pages/auth/login/login.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/auth/login/login.html b/src/app/pages/auth/login/login.html
new file mode 100644
index 0000000..38aab61
--- /dev/null
+++ b/src/app/pages/auth/login/login.html
@@ -0,0 +1,41 @@
+
+ @if(error()) {
+
+
+
Error {{error()}}
+
+ }
+
+
\ No newline at end of file
diff --git a/src/app/pages/auth/login/login.spec.ts b/src/app/pages/auth/login/login.spec.ts
new file mode 100644
index 0000000..8d4c63e
--- /dev/null
+++ b/src/app/pages/auth/login/login.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Login } from './login';
+
+describe('Login', () => {
+ let component: Login;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Login]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Login);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/auth/login/login.ts b/src/app/pages/auth/login/login.ts
new file mode 100644
index 0000000..d82f5d6
--- /dev/null
+++ b/src/app/pages/auth/login/login.ts
@@ -0,0 +1,27 @@
+import { Component, inject, signal } from '@angular/core';
+import { FormsModule, NgForm } from '@angular/forms';
+import { Router, RouterLink } from "@angular/router";
+import { FormUtility } from '@utilities/form-utility';
+import { Authorization } from '@services/authorization/authorization';
+
+@Component({
+ selector: 'app-login',
+ imports: [FormsModule, RouterLink],
+ templateUrl: './login.html',
+ styleUrl: './login.css',
+})
+export class Login extends FormUtility {
+ private authService = inject(Authorization);
+ private router = inject(Router);
+
+ error = signal(null);
+
+
+ onSubmit(form: NgForm) {
+ console.log(form.value);
+ this.authService.login(form.value.email, form.value.password).subscribe({
+ error: (err) => this.error.set(err.message),
+ complete: () => this.router.navigate(['/']),
+ });;
+ }
+}
diff --git a/src/app/pages/auth/signup/signup.css b/src/app/pages/auth/signup/signup.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/auth/signup/signup.html b/src/app/pages/auth/signup/signup.html
new file mode 100644
index 0000000..c9ce9ad
--- /dev/null
+++ b/src/app/pages/auth/signup/signup.html
@@ -0,0 +1 @@
+signup works!
diff --git a/src/app/pages/auth/signup/signup.spec.ts b/src/app/pages/auth/signup/signup.spec.ts
new file mode 100644
index 0000000..1e81683
--- /dev/null
+++ b/src/app/pages/auth/signup/signup.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Signup } from './signup';
+
+describe('Signup', () => {
+ let component: Signup;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [Signup]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(Signup);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/auth/signup/signup.ts b/src/app/pages/auth/signup/signup.ts
new file mode 100644
index 0000000..828eb7e
--- /dev/null
+++ b/src/app/pages/auth/signup/signup.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-signup',
+ imports: [],
+ templateUrl: './signup.html',
+ styleUrl: './signup.css',
+})
+export class Signup {
+
+}
diff --git a/src/app/services/authorization/authorization-call.base.ts b/src/app/services/authorization/authorization-call.base.ts
new file mode 100644
index 0000000..da326ce
--- /dev/null
+++ b/src/app/services/authorization/authorization-call.base.ts
@@ -0,0 +1,10 @@
+import { User } from "@models/user";
+import { Observable } from "rxjs";
+
+export abstract class AuthorizationCallServiceBase {
+
+ abstract login(username: string, password: string): Observable;
+
+ abstract register(username: string, password: string, email: string): Observable;
+
+}
\ No newline at end of file
diff --git a/src/app/services/authorization/authorization-call.fake.ts b/src/app/services/authorization/authorization-call.fake.ts
new file mode 100644
index 0000000..cfae681
--- /dev/null
+++ b/src/app/services/authorization/authorization-call.fake.ts
@@ -0,0 +1,42 @@
+import { Observable } from "rxjs";
+import { AuthorizationCallServiceBase } from "./authorization-call.base";
+import { User } from "@models/user";
+
+export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
+
+
+ private users: User[] = [
+ { id: 1, email: 'testuser@example.com' },
+ ];
+
+ login(email: string, password: string): Observable {
+ return new Observable((observer) => {
+ const user = this.users.find(u => u.email === email);
+ if (!user || password !== 'password') {
+ observer.error(new Error('Invalid username or password'));
+ return;
+ }
+ observer.next(user);
+ observer.complete();
+ });
+ }
+
+ register(username: string, password: string, email: string): Observable {
+ return new Observable((observer) => {
+ // Simulate a successful registration response
+ if (this.users.find(user => user.email === email)) {
+ observer.error(new Error('Email already exists'));
+ return;
+ }
+ this.users.push({ id: this.users.length + 1, email });
+ observer.next();
+ observer.complete();
+ });
+ }
+
+
+
+
+
+
+ }
\ No newline at end of file
diff --git a/src/app/services/authorization/authorization-call.ts b/src/app/services/authorization/authorization-call.ts
new file mode 100644
index 0000000..8c805cb
--- /dev/null
+++ b/src/app/services/authorization/authorization-call.ts
@@ -0,0 +1,20 @@
+import { HttpClient } from "@angular/common/http";
+import { AuthorizationCallServiceBase } from "./authorization-call.base";
+import { inject } from "@angular/core";
+import { User } from "@models/user";
+import { Observable } from "rxjs";
+
+export class AuthorizationCallService extends AuthorizationCallServiceBase {
+
+ private readonly httpClient = inject(HttpClient);
+
+ override login(username: string, password: string): Observable {
+ throw new Error("Method not implemented.");
+ }
+ override register(username: string, password: string, email: string): Observable {
+ throw new Error("Method not implemented.");
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/app/services/authorization/authorization.spec.ts b/src/app/services/authorization/authorization.spec.ts
new file mode 100644
index 0000000..c93d062
--- /dev/null
+++ b/src/app/services/authorization/authorization.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { Authorization } from './authorization';
+
+describe('Authorization', () => {
+ let service: Authorization;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(Authorization);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts
new file mode 100644
index 0000000..9d1fd47
--- /dev/null
+++ b/src/app/services/authorization/authorization.ts
@@ -0,0 +1,41 @@
+import { inject, Injectable, signal } from '@angular/core';
+import { User } from '@models/user';
+import { AuthorizationCallServiceBase } from './authorization-call.base';
+import { environment } from '@environment';
+import { take, tap } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class Authorization {
+
+ private readonly apiUrl = environment.apiUrl + '/auth';
+
+
+ authCall: AuthorizationCallServiceBase = new environment.authorizationCallService();
+
+ user = signal(null);
+
+ login(username: string, password: string) {
+ return this.authCall.login(username, password).pipe(
+ take(1),
+ tap(res => {
+ this.user.set(res);
+ })
+ );
+ }
+
+
+ logout() {
+ this.user.set(null);
+ }
+
+ isLoggedIn(): boolean {
+ return this.user() !== null;
+ }
+
+ getUsername(): string | null {
+ return this.user()?.email || null;
+ }
+
+}
diff --git a/src/app/utilities/form-utility.ts b/src/app/utilities/form-utility.ts
new file mode 100644
index 0000000..7e495f5
--- /dev/null
+++ b/src/app/utilities/form-utility.ts
@@ -0,0 +1,7 @@
+import { NgModel } from "@angular/forms";
+
+export class FormUtility {
+ isInvalid(formControl: NgModel) {
+ return formControl.touched && formControl.dirty && formControl.invalid;
+ }
+}
diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts
new file mode 100644
index 0000000..5c6a7a9
--- /dev/null
+++ b/src/environments/environment.development.ts
@@ -0,0 +1,7 @@
+import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake";
+
+export const environment = {
+ production: false,
+ apiUrl: 'http://localhost:8080',
+ authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..f31dbab
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,7 @@
+import { AuthorizationCallService } from "@services/authorization/authorization-call";
+
+export const environment = {
+ apiUrl: '/api',
+ production: true,
+ authorizationCallService: AuthorizationCallService,
+};
diff --git a/src/styles.css b/src/styles.css
index 35eb9c2..8c4819d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,3 +1,89 @@
/* You can add global styles to this file, and also import other style files */
-@import "tailwindcss";
+@import 'tailwindcss';
+
+@theme {
+ /* Dark mode */
+ --dark-mode-selector: .dark;
+
+ /* Estensioni */
+ --color-primary-50: #eff6ff;
+ --color-primary-100: #dbeafe;
+ --color-primary-200: #bfdbfe;
+ --color-primary-300: #93c5fd;
+ --color-primary-400: #60a5fa;
+ --color-primary-500: #3b82f6;
+ --color-primary-600: #2563eb;
+ --color-primary-700: #1d4ed8;
+ --color-primary-800: #1e40af;
+ --color-primary-900: #1e3a8a;
+ --color-primary-950: #172554;
+
+ /* Font families */
+ --font-body: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
+ Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+
+ --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
+ Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+}
+
+body {
+ @apply bg-gray-50 dark:bg-gray-900;
+}
+
+.container-centered {
+ @apply flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0;
+}
+
+.input {
+ @apply bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500;
+}
+
+.card-container {
+ @apply w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700;
+}
+.card {
+ @apply p-6 space-y-4 md:space-y-6 sm:p-8;
+}
+.label {
+ @apply block mb-2 text-sm font-medium text-gray-900 dark:text-white;
+}
+
+.title {
+ @apply text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white;
+}
+
+.button {
+ @apply text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800;
+}
+
+.button:disabled {
+ @apply bg-primary-500 opacity-40;
+}
+.link {
+ @apply font-medium text-primary-600 hover:underline dark:text-primary-500;
+}
+.link-active {
+ @apply text-purple-600 dark:text-white;
+}
+
+.form-control {
+ @apply flex flex-col gap-0.5;
+}
+
+.form-group {
+ @apply flex flex-col gap-4;
+}
+
+.helper-text {
+ @apply text-sm font-light text-gray-500 dark:text-gray-400;
+}
+
+.nav-header {
+ @apply bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800;
+}
+.inner-nav {
+ @apply flex flex-wrap justify-center gap-4 items-center max-w-7xl;
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 2ab7442..bd9e69c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,16 @@
{
"compileOnSave": false,
"compilerOptions": {
+ "baseUrl": "./src",
+ "paths": {
+ "@pages/*" : ["app/pages/*"],
+ "@layouts/*" : ["app/layouts/*"],
+ "@services/*" : ["app/services/*"],
+ "@environment" : ["environments/environment"],
+ "@models/*" : ["app/models/*"],
+ "@utilities/*" : ["app/utilities/*"],
+ "@guards/*" : ["app/guards/*"]
+ },
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,