@ichgamer999/wmctest 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app-expenses/app.component.css +23 -0
- package/src/app-expenses/app.component.html +19 -0
- package/src/app-expenses/app.component.ts +26 -0
- package/src/app-expenses/app.config.ts +12 -0
- package/src/app-expenses/app.routes.ts +9 -0
- package/src/app-expenses/components/expense/expense.component.css +8 -0
- package/src/app-expenses/components/expense/expense.component.html +42 -0
- package/src/app-expenses/components/expense/expense.component.ts +97 -0
- package/src/app-expenses/components/login/login.component.css +24 -0
- package/src/app-expenses/components/login/login.component.html +13 -0
- package/src/app-expenses/components/login/login.component.ts +34 -0
- package/src/app-expenses/interceptor/auth.interceptor.ts +14 -0
- package/src/app-expenses/services/auth.service.ts +31 -0
- package/src/app-expenses/services/http-service.service.ts +48 -0
- package/src/app-expenses/types/Expense.ts +18 -0
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.layout {
|
|
2
|
+
display: flex;
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.sidebar {
|
|
7
|
+
width: 180px;
|
|
8
|
+
padding: 8px;
|
|
9
|
+
background: #e0e0e0;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: 8px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.content {
|
|
16
|
+
flex: 1;
|
|
17
|
+
padding: 8px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
button {
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div class="layout">
|
|
2
|
+
<div class="sidebar">
|
|
3
|
+
@if (auth.isAuthenticated()) {
|
|
4
|
+
<div>{{ auth.getUserName() }}</div>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
<a href="/expenses">Expenses</a>
|
|
8
|
+
|
|
9
|
+
@if (!auth.isAuthenticated()) {
|
|
10
|
+
<button (click)="login()">Login</button>
|
|
11
|
+
} @else {
|
|
12
|
+
<button (click)="logout()">LogOut</button>
|
|
13
|
+
}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="content">
|
|
17
|
+
<router-outlet></router-outlet>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {Component, inject} from '@angular/core';
|
|
2
|
+
import {LoginComponent} from "./components/login/login.component";
|
|
3
|
+
import {Router, RouterOutlet} from "@angular/router";
|
|
4
|
+
import {AuthService} from "./services/auth.service";
|
|
5
|
+
import {HttpServiceService} from "./services/http-service.service";
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-root',
|
|
9
|
+
imports: [
|
|
10
|
+
RouterOutlet
|
|
11
|
+
],
|
|
12
|
+
templateUrl: './app.component.html',
|
|
13
|
+
styleUrl: './app.component.css'
|
|
14
|
+
})
|
|
15
|
+
export class AppComponent {
|
|
16
|
+
auth = inject(AuthService)
|
|
17
|
+
router = inject(Router);
|
|
18
|
+
http = inject(HttpServiceService)
|
|
19
|
+
login(){
|
|
20
|
+
this.router.navigate(['/login']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
logout(){
|
|
24
|
+
this.http.logout()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {ApplicationConfig, provideZonelessChangeDetection} from '@angular/core';
|
|
2
|
+
import {provideRouter} from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import {routes} from './app.routes';
|
|
5
|
+
import {provideHttpClient, withInterceptors} from "@angular/common/http";
|
|
6
|
+
import {authInterceptor} from "./interceptor/auth.interceptor";
|
|
7
|
+
|
|
8
|
+
export const appConfig: ApplicationConfig = {
|
|
9
|
+
providers: [provideZonelessChangeDetection(),
|
|
10
|
+
provideHttpClient(withInterceptors([ authInterceptor])),
|
|
11
|
+
provideRouter(routes)]
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import {LoginComponent} from "./components/login/login.component";
|
|
3
|
+
import {ExpenseComponent} from "./components/expense/expense.component";
|
|
4
|
+
|
|
5
|
+
export const routes: Routes = [
|
|
6
|
+
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
|
7
|
+
{ path: 'login', component: LoginComponent },
|
|
8
|
+
{ path: 'expenses', component: ExpenseComponent}
|
|
9
|
+
];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<h2>My Expenses</h2>
|
|
2
|
+
|
|
3
|
+
@if (editingExpense(); as expenseToEdit) {
|
|
4
|
+
<form (ngSubmit)="saveEdit()">
|
|
5
|
+
<h3>Edit Expense</h3>
|
|
6
|
+
<div>
|
|
7
|
+
<label for="type">Type</label>
|
|
8
|
+
<input id="type" name="type" [(ngModel)]="expenseToEdit.type" />
|
|
9
|
+
</div>
|
|
10
|
+
<div>
|
|
11
|
+
<label for="description">Description</label>
|
|
12
|
+
<input id="description" name="description" [(ngModel)]="expenseToEdit.description" />
|
|
13
|
+
</div>
|
|
14
|
+
<div>
|
|
15
|
+
<label for="amount">Amount</label>
|
|
16
|
+
<input id="amount" name="amount" type="number" [(ngModel)]="expenseToEdit.amount" />
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<label for="expenseDate">Date</label>
|
|
20
|
+
<input id="expenseDate" name="expenseDate" type="date" [(ngModel)]="expenseToEdit.expenseDate" />
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<button type="submit">Save</button>
|
|
24
|
+
<button type="button" (click)="cancelEdit()">Cancel</button>
|
|
25
|
+
</div>
|
|
26
|
+
</form>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@for (expense of this.expenses(); track expense.clientId ?? $index) {
|
|
30
|
+
<div class="expense-row">
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<div [style.color]="isFoodType(expense.type)">{{ expense.type }}</div>
|
|
34
|
+
<div>{{ expense.description }}</div>
|
|
35
|
+
<div>${{ expense.amount }}</div>
|
|
36
|
+
<div>{{ expense.expenseDate }}</div>
|
|
37
|
+
<div>
|
|
38
|
+
<button type="button" (click)="startEdit(expense)">Edit</button>
|
|
39
|
+
<button type="button" (click)="deleteExpense(expense)">Delete</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {Component, inject, OnInit, signal} from '@angular/core';
|
|
2
|
+
import {AuthService} from "../../services/auth.service";
|
|
3
|
+
import {HttpServiceService} from "../../services/http-service.service";
|
|
4
|
+
import {Expense} from "../../types/Expense";
|
|
5
|
+
import {FormsModule} from "@angular/forms";
|
|
6
|
+
import {Router} from "@angular/router";
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-expense',
|
|
10
|
+
imports: [
|
|
11
|
+
FormsModule
|
|
12
|
+
],
|
|
13
|
+
templateUrl: './expense.component.html',
|
|
14
|
+
styleUrl: './expense.component.css',
|
|
15
|
+
})
|
|
16
|
+
export class ExpenseComponent implements OnInit {
|
|
17
|
+
|
|
18
|
+
authService = inject(AuthService)
|
|
19
|
+
http = inject(HttpServiceService)
|
|
20
|
+
expenses = signal<Expense[]>([])
|
|
21
|
+
editingExpense = signal<Expense | null>(null)
|
|
22
|
+
editingExpenseId = signal<number | null>(null)
|
|
23
|
+
private nextClientId = 0;
|
|
24
|
+
router = inject(Router);
|
|
25
|
+
|
|
26
|
+
ngOnInit(): void {
|
|
27
|
+
if (this.authService.isAuthenticated()) {
|
|
28
|
+
this.http.getAllExpenses().subscribe(
|
|
29
|
+
value => {
|
|
30
|
+
this.expenses.set(
|
|
31
|
+
value.map(expense => ({
|
|
32
|
+
...expense,
|
|
33
|
+
clientId: this.nextClientId++,
|
|
34
|
+
}))
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
} else {
|
|
39
|
+
this.router.navigate(["/login"])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
startEdit(expense: Expense) {
|
|
44
|
+
this.editingExpenseId.set(expense.clientId ?? null)
|
|
45
|
+
this.editingExpense.set({ ...expense })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
saveEdit() {
|
|
49
|
+
const expenseId = this.editingExpenseId()
|
|
50
|
+
const expense = this.editingExpense()
|
|
51
|
+
|
|
52
|
+
if (expenseId === null || !expense) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.expenses.update(currentExpenses =>
|
|
57
|
+
currentExpenses.map(currentExpense =>
|
|
58
|
+
currentExpense.clientId === expenseId ? { ...currentExpense, ...expense, clientId: expenseId } : currentExpense
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
this.cancelEdit()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
deleteExpense(expense: Expense) {
|
|
66
|
+
const expenseId = expense.clientId
|
|
67
|
+
|
|
68
|
+
if (expenseId === undefined) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const confirmed = window.confirm(`Möchtest du ${expense.type} wirklich löschen?`)
|
|
73
|
+
|
|
74
|
+
if (!confirmed) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.expenses.update(currentExpenses => currentExpenses.filter(currentExpense => currentExpense.clientId !== expenseId))
|
|
79
|
+
|
|
80
|
+
if (this.editingExpenseId() === expenseId) {
|
|
81
|
+
this.cancelEdit()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cancelEdit() {
|
|
86
|
+
this.editingExpenseId.set(null)
|
|
87
|
+
this.editingExpense.set(null)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
isFoodType(type: string | undefined) {
|
|
91
|
+
if (type?.toLowerCase() == 'food') {
|
|
92
|
+
return 'green'
|
|
93
|
+
}
|
|
94
|
+
else return 'blue'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
form {
|
|
2
|
+
max-width: 320px;
|
|
3
|
+
margin: 16px 0;
|
|
4
|
+
padding: 8px;
|
|
5
|
+
border: 1px solid #999;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
label {
|
|
9
|
+
display: block;
|
|
10
|
+
margin-bottom: 8px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
input {
|
|
14
|
+
width: 100%;
|
|
15
|
+
margin-top: 2px;
|
|
16
|
+
margin-bottom: 8px;
|
|
17
|
+
border: 1px solid #999;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
button {
|
|
21
|
+
width: 100%;
|
|
22
|
+
border: 1px solid #666;
|
|
23
|
+
background: #d9d9d9;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<form [formGroup]="loginModel" (ngSubmit)="onSubmit()">
|
|
2
|
+
|
|
3
|
+
<label>
|
|
4
|
+
Username
|
|
5
|
+
<input type="text" formControlName="username" />
|
|
6
|
+
</label>
|
|
7
|
+
<label>
|
|
8
|
+
Password
|
|
9
|
+
<input type="password" formControlName="password" />
|
|
10
|
+
</label>
|
|
11
|
+
<button type="submit" [disabled]="loginModel.invalid">Login</button>
|
|
12
|
+
|
|
13
|
+
</form>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {Component, inject, signal} from '@angular/core';
|
|
2
|
+
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
|
|
3
|
+
import {HttpServiceService} from "../../services/http-service.service";
|
|
4
|
+
import {Router} from "@angular/router";
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-login',
|
|
8
|
+
imports: [
|
|
9
|
+
FormsModule,
|
|
10
|
+
ReactiveFormsModule
|
|
11
|
+
],
|
|
12
|
+
templateUrl: './login.component.html',
|
|
13
|
+
styleUrl: './login.component.css',
|
|
14
|
+
})
|
|
15
|
+
export class LoginComponent {
|
|
16
|
+
http = inject(HttpServiceService);
|
|
17
|
+
router = inject(Router);
|
|
18
|
+
loginModel = new FormGroup({
|
|
19
|
+
'username': new FormControl('', Validators.required),
|
|
20
|
+
'password': new FormControl('', Validators.required),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onSubmit() {
|
|
24
|
+
if (this.loginModel.valid) {
|
|
25
|
+
const {username, password} = this.loginModel.value;
|
|
26
|
+
|
|
27
|
+
if (username && password) {
|
|
28
|
+
this.http.login(username, password).subscribe({
|
|
29
|
+
next: () => this.router.navigate(['/expenses']),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HttpInterceptorFn } from '@angular/common/http';
|
|
2
|
+
import {inject} from "@angular/core";
|
|
3
|
+
import {AuthService} from "../services/auth.service";
|
|
4
|
+
|
|
5
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
6
|
+
const authService = inject(AuthService)
|
|
7
|
+
if(!authService.isAuthenticated()){
|
|
8
|
+
return next(req);
|
|
9
|
+
}
|
|
10
|
+
let clone = req.clone({
|
|
11
|
+
headers: req.headers.set('Authorization', `Bearer ${authService.getToken()}`)
|
|
12
|
+
});
|
|
13
|
+
return next(clone);
|
|
14
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root',
|
|
5
|
+
})
|
|
6
|
+
export class AuthService {
|
|
7
|
+
private is_authenticated = "";
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
const savedUser = localStorage.getItem("userName");
|
|
11
|
+
if (savedUser) {
|
|
12
|
+
this.is_authenticated = savedUser;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
isAuthenticated(){
|
|
17
|
+
return this.is_authenticated != "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setAuthenticated(newState: string){
|
|
21
|
+
this.is_authenticated = newState;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getUserName(){
|
|
25
|
+
return this.is_authenticated
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getToken(){
|
|
29
|
+
return localStorage.getItem('secretAuthToken');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {inject, Injectable} from '@angular/core';
|
|
2
|
+
import {HttpClient} from "@angular/common/http";
|
|
3
|
+
import {AuthService} from "./auth.service";
|
|
4
|
+
import {Router} from "@angular/router";
|
|
5
|
+
import {Observable, tap} from "rxjs";
|
|
6
|
+
import {Expense} from "../types/Expense";
|
|
7
|
+
|
|
8
|
+
@Injectable({
|
|
9
|
+
providedIn: 'root',
|
|
10
|
+
})
|
|
11
|
+
export class HttpServiceService {
|
|
12
|
+
http = inject(HttpClient);
|
|
13
|
+
authService = inject(AuthService);
|
|
14
|
+
router = inject(Router);
|
|
15
|
+
private baseURL = "http://localhost:8080/api";
|
|
16
|
+
|
|
17
|
+
login(username: string, password: string){
|
|
18
|
+
return this.http
|
|
19
|
+
.post<{ token: string }>(`${this.baseURL}/token`,
|
|
20
|
+
{ username: username, password: password })
|
|
21
|
+
.pipe(
|
|
22
|
+
tap(response => {
|
|
23
|
+
localStorage.setItem('secretAuthToken', response.token);
|
|
24
|
+
localStorage.setItem("userName", username)
|
|
25
|
+
this.authService.setAuthenticated(username);
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getAllExpenses(): Observable<Expense[]> {
|
|
31
|
+
return this.http.get<Expense[]>(`${this.baseURL}/expenses`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
updateExpense(expenseId: number, expense: Expense): Observable<Expense> {
|
|
35
|
+
return this.http.put<Expense>(`${this.baseURL}/expenses/${expenseId}`, expense);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
deleteExpense(expenseId: number): Observable<void> {
|
|
39
|
+
return this.http.delete<void>(`${this.baseURL}/expenses/${expenseId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logout(){
|
|
43
|
+
localStorage.removeItem('secretAuthToken');
|
|
44
|
+
this.authService.setAuthenticated("");
|
|
45
|
+
this.router.navigate(['/login']);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class Expense {
|
|
2
|
+
id?: number
|
|
3
|
+
clientId?: number
|
|
4
|
+
type: string
|
|
5
|
+
amount: number
|
|
6
|
+
expenseDate: string
|
|
7
|
+
description: string
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
constructor(type: string, amount: number, expenseDate: string, description: string, id?: number, clientId?: number) {
|
|
11
|
+
this.id = id;
|
|
12
|
+
this.clientId = clientId;
|
|
13
|
+
this.type = type;
|
|
14
|
+
this.amount = amount;
|
|
15
|
+
this.expenseDate = expenseDate;
|
|
16
|
+
this.description = description;
|
|
17
|
+
}
|
|
18
|
+
}
|