Skip to content

Commit 711ebbd

Browse files
committed
add AsyncState<T,E> pattern & migrate auth facades from boolean flags
- Add AsyncState type with initial/loading/success/failure states, type guards and factory functions - Add toAsyncState() RxJS operator for automatic Result→AsyncState mapping - Add PasswordError type for reset/set password use-cases - Replace boolean loading/error signals with typed AsyncState in auth-ui-info - Migrate auth-login, auth-register, auth-password facades to use toAsyncState() pipe - Convert serverErrors from manual signal to computed derived from AsyncState
1 parent 1811327 commit 711ebbd

7 files changed

Lines changed: 188 additions & 104 deletions

File tree

projects/social_platform/src/app/api/auth/facades/auth-login.service.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
import { inject, Injectable } from "@angular/core";
44
import { ActivatedRoute, Router } from "@angular/router";
55
import { TokenService, ValidationService } from "@corelib";
6-
import { Subject, takeUntil } from "rxjs";
6+
import { Subject, takeUntil, tap } from "rxjs";
77
import { AuthUIInfoService } from "./ui/auth-ui-info.service";
88
import { LoggerService } from "projects/core/src/lib/services/logger/logger.service";
9-
import { AuthRepository } from "../../../infrastructure/repository/auth/auth.repository";
109
import { LoginUseCase } from "../use-cases/login.use-case";
10+
import { toAsyncState } from "projects/social_platform/src/app/domain/shared/to-async-state";
11+
import {
12+
LoginResult,
13+
LoginError,
14+
} from "projects/social_platform/src/app/domain/auth/results/login.result";
1115

1216
@Injectable()
1317
export class AuthLoginService {
1418
private readonly tokenService = inject(TokenService);
15-
private readonly authRepository = inject(AuthRepository);
1619
private readonly route = inject(ActivatedRoute);
1720
private readonly router = inject(Router);
1821
private readonly validationService = inject(ValidationService);
@@ -26,8 +29,7 @@ export class AuthLoginService {
2629

2730
// Login Component
2831
readonly showPassword = this.authUIInfoService.showPassword;
29-
readonly loginIsSubmitting = this.authUIInfoService.loginIsSubmitting;
30-
readonly errorWrongAuth = this.authUIInfoService.errorWrongAuth;
32+
readonly login$ = this.authUIInfoService.login$;
3133

3234
destroy(): void {
3335
this.destroy$.next();
@@ -37,34 +39,28 @@ export class AuthLoginService {
3739
onSubmit() {
3840
const redirectType = this.route.snapshot.queryParams["redirect"];
3941

40-
if (!this.validationService.getFormValidation(this.loginForm) || this.loginIsSubmitting()) {
42+
if (
43+
!this.validationService.getFormValidation(this.loginForm) ||
44+
this.login$().status === "loading"
45+
) {
4146
return;
4247
}
4348

44-
this.loginIsSubmitting.set(true);
45-
this.errorWrongAuth.set(false);
46-
4749
this.loginUseCase
4850
.execute(this.loginForm.getRawValue())
49-
.pipe(takeUntil(this.destroy$))
50-
.subscribe({
51-
next: result => {
52-
this.loginIsSubmitting.set(false);
53-
54-
if (!result.ok) {
55-
if (result.error.kind === "wrong_credentials") {
56-
this.errorWrongAuth.set(true);
57-
}
58-
return;
51+
.pipe(
52+
tap(result => {
53+
if (result.ok) {
54+
this.tokenService.memTokens(result.value.tokens);
55+
const url = redirectType === "program" ? "/office/program/list" : "/office";
56+
this.router
57+
.navigateByUrl(url)
58+
.then(() => this.logger.debug("Route changed from LoginComponent"));
5959
}
60-
61-
this.tokenService.memTokens(result.value.tokens);
62-
63-
const url = redirectType === "program" ? "/office/program/list" : "/office";
64-
this.router
65-
.navigateByUrl(url)
66-
.then(() => this.logger.debug("Route changed from LoginComponent"));
67-
},
68-
});
60+
}),
61+
toAsyncState<LoginResult, LoginError>(),
62+
takeUntil(this.destroy$)
63+
)
64+
.subscribe(state => this.login$.set(state));
6965
}
7066
}

projects/social_platform/src/app/api/auth/facades/auth-password.service.ts

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import { inject, Injectable } from "@angular/core";
44
import { ActivatedRoute, Router } from "@angular/router";
55
import { ValidationService } from "@corelib";
6-
import { map, Subject, takeUntil } from "rxjs";
6+
import { map, Subject, takeUntil, tap } from "rxjs";
77
import { AuthUIInfoService } from "./ui/auth-ui-info.service";
88
import { LoggerService } from "projects/core/src/lib/services/logger/logger.service";
99
import { ResetPasswordUseCase } from "../use-cases/reset-password.use-case";
1010
import { SetPasswordUseCase } from "../use-cases/set-password.use-case";
11+
import { toAsyncState } from "../../../domain/shared/to-async-state";
12+
import { PasswordError } from "../../../domain/auth/results/password.result";
1113

1214
@Injectable()
1315
export class AuthPasswordService {
@@ -31,10 +33,9 @@ export class AuthPasswordService {
3133
);
3234

3335
// ResetPassword Component
34-
readonly isSubmitting = this.authUIInfoService.isSubmitting;
35-
readonly errorRequest = this.authUIInfoService.errorRequest;
36+
readonly password$ = this.authUIInfoService.password$;
37+
3638
readonly credsSubmitInitiated = this.authUIInfoService.credsSubmitInitiated;
37-
private readonly errorServer = this.authUIInfoService.errorServer;
3839

3940
init(): void {
4041
const token = this.route.snapshot.queryParamMap.get("token");
@@ -46,57 +47,60 @@ export class AuthPasswordService {
4647

4748
// ResetPassword Component
4849
onSubmitResetPassword(): void {
49-
if (!this.validationService.getFormValidation(this.resetForm)) return;
50-
51-
this.errorServer.set(false);
52-
this.isSubmitting.set(true);
50+
if (
51+
!this.validationService.getFormValidation(this.resetForm) ||
52+
this.password$().status === "loading"
53+
)
54+
return;
5355

5456
this.resetPasswordUseCase
5557
.execute(this.resetForm.value.email!)
56-
.pipe(takeUntil(this.destroy$))
57-
.subscribe({
58-
next: result => {
59-
if (!result.ok) {
60-
this.errorServer.set(true);
58+
.pipe(
59+
tap(result => {
60+
if (result.ok) {
61+
this.router
62+
.navigate(["/auth/reset_password/confirm"], {
63+
queryParams: { email: this.resetForm.value.email },
64+
})
65+
.then(() => this.logger.debug("ResetPasswordComponent"));
66+
} else {
6167
this.resetForm.reset();
62-
return;
6368
}
64-
65-
this.router
66-
.navigate(["/auth/reset_password/confirm"], {
67-
queryParams: { email: this.resetForm.value.email },
68-
})
69-
.then(() => this.logger.debug("ResetPasswordComponent"));
70-
},
71-
complete: () => {
72-
this.isSubmitting.set(false);
73-
},
74-
});
69+
}),
70+
toAsyncState<void, PasswordError>(),
71+
takeUntil(this.destroy$)
72+
)
73+
.subscribe({ next: result => this.password$.set(result) });
7574
}
7675

7776
// SetPassword Component
7877
onSubmitSetPassword(): void {
7978
this.credsSubmitInitiated.set(true);
8079
const token = this.route.snapshot.queryParamMap.get("token");
8180

82-
if (!token || !this.validationService.getFormValidation(this.passwordForm)) return;
81+
if (
82+
!token ||
83+
!this.validationService.getFormValidation(this.passwordForm) ||
84+
this.password$().status === "loading"
85+
)
86+
return;
8387

8488
this.setPasswordUseCase
8589
.execute(this.passwordForm.value.password!, token)
86-
.pipe(takeUntil(this.destroy$))
87-
.subscribe({
88-
next: result => {
89-
if (!result.ok) {
90-
this.logger.error("Error setting password:", result.error.cause);
91-
this.errorRequest.set(true);
92-
return;
90+
.pipe(
91+
tap(result => {
92+
if (result.ok) {
93+
this.router
94+
.navigateByUrl("/auth/login")
95+
.then(() => this.logger.debug("SetPasswordComponent"));
96+
} else {
97+
this.logger.error("Error setting password:", result.error);
9398
}
94-
95-
this.router
96-
.navigateByUrl("/auth/login")
97-
.then(() => this.logger.debug("SetPasswordComponent"));
98-
},
99-
});
99+
}),
100+
toAsyncState<void, PasswordError>(),
101+
takeUntil(this.destroy$)
102+
)
103+
.subscribe({ next: result => this.password$.set(result) });
100104
}
101105

102106
destroy(): void {
Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/** @format */
22

3-
import { inject, Injectable, signal } from "@angular/core";
3+
import { computed, inject, Injectable } from "@angular/core";
44
import { Router } from "@angular/router";
5-
import { Subject, takeUntil } from "rxjs";
5+
import { Subject, takeUntil, tap } from "rxjs";
66
import { AuthUIInfoService } from "./ui/auth-ui-info.service";
77
import { ValidationService } from "@corelib";
88
import { RegisterUseCase } from "../use-cases/register.use-case";
9+
import { toAsyncState } from "../../../domain/shared/to-async-state";
10+
import { RegisterError } from "../../../domain/auth/results/register.result";
11+
import { isFailure } from "../../../domain/shared/async-state";
912

1013
@Injectable()
1114
export class AuthRegisterService {
@@ -18,18 +21,24 @@ export class AuthRegisterService {
1821

1922
readonly registerAgreement = this.authUIInfoService.registerAgreement;
2023
readonly ageAgreement = this.authUIInfoService.ageAgreement;
21-
readonly registerIsSubmitting = this.authUIInfoService.registerIsSubmitting;
2224
readonly credsSubmitInitiated = this.authUIInfoService.credsSubmitInitiated;
2325
readonly infoSubmitInitiated = this.authUIInfoService.infoSubmitInitiated;
2426

2527
readonly showPassword = this.authUIInfoService.showPassword;
2628
readonly showPasswordRepeat = this.authUIInfoService.showPasswordRepeat;
2729

28-
readonly isUserCreationModalError = this.authUIInfoService.isUserCreationModalError;
30+
readonly register$ = this.authUIInfoService.register$;
2931

3032
readonly step = this.authUIInfoService.step;
3133

32-
readonly serverErrors = signal<string[]>([]);
34+
// Вычисляемое из AsyncState — автоматически обновляется при смене состояния
35+
readonly serverErrors = computed(() => {
36+
const state = this.register$();
37+
if (isFailure(state) && state.error.kind === "validation_error") {
38+
return Object.values(state.error.errors).flat();
39+
}
40+
return [];
41+
});
3342

3443
private readonly destroy$ = new Subject<void>();
3544

@@ -39,36 +48,26 @@ export class AuthRegisterService {
3948
}
4049

4150
onSendForm(): void {
42-
if (!this.validationService.getFormValidation(this.registerForm)) {
51+
if (
52+
!this.validationService.getFormValidation(this.registerForm) ||
53+
this.register$().status === "loading"
54+
) {
4355
return;
4456
}
4557

4658
const form = this.authUIInfoService.prepareFormValues(this.registerForm);
4759

48-
this.serverErrors.set([]);
49-
this.isUserCreationModalError.set(false);
50-
this.registerIsSubmitting.set(true);
51-
5260
this.registerUseCase
5361
.execute(form)
54-
.pipe(takeUntil(this.destroy$))
55-
.subscribe({
56-
next: result => {
57-
if (!result.ok) {
58-
if (result.error.kind === "validation_error") {
59-
this.serverErrors.set(Object.values(result.error.errors).flat());
60-
} else if (result.error.kind === "server_error") {
61-
this.isUserCreationModalError.set(true);
62-
}
63-
64-
this.registerIsSubmitting.set(false);
65-
66-
return;
62+
.pipe(
63+
tap(result => {
64+
if (result.ok) {
65+
this.router.navigateByUrl("/auth/verification/email?adress=" + form.email);
6766
}
68-
69-
this.registerIsSubmitting.set(false);
70-
this.router.navigateByUrl("/auth/verification/email?adress=" + form.email);
71-
},
72-
});
67+
}),
68+
toAsyncState<void, RegisterError>(),
69+
takeUntil(this.destroy$)
70+
)
71+
.subscribe(state => this.register$.set(state));
7372
}
7473
}

projects/social_platform/src/app/api/auth/facades/ui/auth-ui-info.service.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { inject, Injectable, signal } from "@angular/core";
44
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
55
import { ValidationService } from "@corelib";
66
import dayjs from "dayjs";
7+
import {
8+
LoginError,
9+
LoginResult,
10+
} from "projects/social_platform/src/app/domain/auth/results/login.result";
11+
import { PasswordError } from "projects/social_platform/src/app/domain/auth/results/password.result";
12+
import { RegisterError } from "projects/social_platform/src/app/domain/auth/results/register.result";
13+
import { AsyncState, initial } from "projects/social_platform/src/app/domain/shared/async-state";
714

815
@Injectable()
916
export class AuthUIInfoService {
@@ -14,24 +21,18 @@ export class AuthUIInfoService {
1421
readonly showPasswordRepeat = signal<boolean>(false);
1522
readonly showPassword = signal<boolean>(false);
1623

17-
readonly loginIsSubmitting = signal<boolean>(false);
18-
readonly errorWrongAuth = signal<boolean>(false);
24+
readonly login$ = signal<AsyncState<LoginResult, LoginError>>(initial());
1925

2026
// password
21-
readonly isSubmitting = signal<boolean>(false);
2227
readonly credsSubmitInitiated = signal<boolean>(false);
23-
24-
readonly errorServer = signal<boolean>(false);
25-
readonly errorRequest = signal<boolean>(false);
28+
readonly password$ = signal<AsyncState<void, PasswordError>>(initial());
2629

2730
// register
2831
readonly registerAgreement = signal<boolean>(false);
2932
readonly ageAgreement = signal<boolean>(false);
30-
31-
readonly registerIsSubmitting = signal<boolean>(false);
3233
readonly infoSubmitInitiated = signal<boolean>(false);
3334

34-
readonly isUserCreationModalError = signal<boolean>(false);
35+
readonly register$ = signal<AsyncState<void, RegisterError>>(initial());
3536
readonly step = signal<"credentials" | "info">("credentials");
3637

3738
// login
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @format */
2+
3+
export type PasswordError =
4+
| { kind: "server_error" }
5+
| { kind: "invalid_token" }
6+
| { kind: "unknown"; cause?: unknown };

0 commit comments

Comments
 (0)