@blackcode_sa/metaestetics-api 1.7.41 → 1.7.42
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/dist/admin/index.d.mts +4 -4
- package/dist/admin/index.d.ts +4 -4
- package/dist/backoffice/index.d.mts +4 -4
- package/dist/backoffice/index.d.ts +4 -4
- package/dist/index.d.mts +103 -98
- package/dist/index.d.ts +103 -98
- package/dist/index.js +454 -374
- package/dist/index.mjs +581 -500
- package/package.json +1 -1
- package/src/services/auth/index.ts +21 -0
- package/src/services/auth/utils/error.utils.ts +90 -0
- package/src/services/auth/utils/firebase.utils.ts +49 -0
- package/src/services/auth/utils/index.ts +21 -0
- package/src/services/auth/utils/practitioner.utils.ts +125 -0
- package/src/services/auth.service.ts +143 -155
- package/src/services/practitioner/practitioner.service.ts +5 -3
- package/src/types/practitioner/index.ts +4 -4
- package/src/validations/practitioner.schema.ts +7 -4
|
@@ -17,18 +17,24 @@ import {
|
|
|
17
17
|
sendPasswordResetEmail,
|
|
18
18
|
verifyPasswordResetCode,
|
|
19
19
|
confirmPasswordReset,
|
|
20
|
+
fetchSignInMethodsForEmail,
|
|
20
21
|
} from "firebase/auth";
|
|
21
22
|
import {
|
|
23
|
+
getFirestore,
|
|
24
|
+
collection,
|
|
22
25
|
doc,
|
|
23
|
-
setDoc,
|
|
24
26
|
getDoc,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
setDoc,
|
|
28
|
+
updateDoc,
|
|
29
|
+
deleteDoc,
|
|
27
30
|
query,
|
|
28
31
|
where,
|
|
29
32
|
getDocs,
|
|
33
|
+
orderBy,
|
|
34
|
+
limit,
|
|
35
|
+
startAfter,
|
|
30
36
|
Timestamp,
|
|
31
|
-
|
|
37
|
+
runTransaction,
|
|
32
38
|
Firestore,
|
|
33
39
|
} from "firebase/firestore";
|
|
34
40
|
import { FirebaseApp } from "firebase/app";
|
|
@@ -72,6 +78,15 @@ import { PractitionerService } from "./practitioner/practitioner.service";
|
|
|
72
78
|
import { practitionerSignupSchema } from "../validations/practitioner.schema";
|
|
73
79
|
import { CertificationLevel } from "../backoffice/types/static/certification.types";
|
|
74
80
|
import { MediaService } from "./media/media.service";
|
|
81
|
+
// Import utility functions
|
|
82
|
+
import {
|
|
83
|
+
checkEmailExists,
|
|
84
|
+
cleanupFirebaseUser,
|
|
85
|
+
handleFirebaseError,
|
|
86
|
+
handleSignupError,
|
|
87
|
+
buildPractitionerData,
|
|
88
|
+
validatePractitionerProfileData,
|
|
89
|
+
} from "./auth/utils";
|
|
75
90
|
|
|
76
91
|
export class AuthService extends BaseService {
|
|
77
92
|
private googleProvider = new GoogleAuthProvider();
|
|
@@ -83,15 +98,10 @@ export class AuthService extends BaseService {
|
|
|
83
98
|
db: Firestore,
|
|
84
99
|
auth: Auth,
|
|
85
100
|
app: FirebaseApp,
|
|
86
|
-
userService
|
|
101
|
+
userService: UserService
|
|
87
102
|
) {
|
|
88
103
|
super(db, auth, app);
|
|
89
|
-
|
|
90
|
-
// Kreiramo UserService ako nije prosleđen
|
|
91
|
-
if (!userService) {
|
|
92
|
-
userService = new UserService(db, auth, app);
|
|
93
|
-
}
|
|
94
|
-
this.userService = userService;
|
|
104
|
+
this.userService = userService || new UserService(db, auth, app);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
/**
|
|
@@ -155,7 +165,7 @@ export class AuthService extends BaseService {
|
|
|
155
165
|
});
|
|
156
166
|
} catch (firebaseError) {
|
|
157
167
|
console.error("[AUTH] Firebase user creation failed:", firebaseError);
|
|
158
|
-
throw firebaseError;
|
|
168
|
+
throw handleFirebaseError(firebaseError);
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
// Create user with CLINIC_ADMIN role
|
|
@@ -879,8 +889,8 @@ export class AuthService extends BaseService {
|
|
|
879
889
|
}
|
|
880
890
|
|
|
881
891
|
/**
|
|
882
|
-
* Registers a new practitioner user with email and password
|
|
883
|
-
*
|
|
892
|
+
* Registers a new practitioner user with email and password (ATOMIC VERSION)
|
|
893
|
+
* Uses Firestore transactions to ensure atomicity and proper rollback on failures
|
|
884
894
|
*
|
|
885
895
|
* @param data - Practitioner signup data containing either new profile details or token for claiming draft profile
|
|
886
896
|
* @returns Object containing the created user and practitioner profile
|
|
@@ -896,27 +906,19 @@ export class AuthService extends BaseService {
|
|
|
896
906
|
user: User;
|
|
897
907
|
practitioner: Practitioner;
|
|
898
908
|
}> {
|
|
909
|
+
let firebaseUser: any = null;
|
|
910
|
+
|
|
899
911
|
try {
|
|
900
|
-
console.log("[AUTH] Starting practitioner signup process", {
|
|
912
|
+
console.log("[AUTH] Starting atomic practitioner signup process", {
|
|
901
913
|
email: data.email,
|
|
902
914
|
hasToken: !!data.token,
|
|
903
915
|
});
|
|
904
916
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
await practitionerSignupSchema.parseAsync(data);
|
|
908
|
-
console.log("[AUTH] Practitioner signup data validation passed");
|
|
909
|
-
} catch (validationError) {
|
|
910
|
-
console.error(
|
|
911
|
-
"[AUTH] Validation error in signUpPractitioner:",
|
|
912
|
-
validationError
|
|
913
|
-
);
|
|
914
|
-
throw validationError;
|
|
915
|
-
}
|
|
917
|
+
// Step 1: Pre-validate all data before any mutations
|
|
918
|
+
await this.validateSignupData(data);
|
|
916
919
|
|
|
917
|
-
// Create Firebase user
|
|
920
|
+
// Step 2: Create Firebase user (outside transaction - can't be easily rolled back)
|
|
918
921
|
console.log("[AUTH] Creating Firebase user");
|
|
919
|
-
let firebaseUser;
|
|
920
922
|
try {
|
|
921
923
|
const result = await createUserWithEmailAndPassword(
|
|
922
924
|
this.auth,
|
|
@@ -929,159 +931,145 @@ export class AuthService extends BaseService {
|
|
|
929
931
|
});
|
|
930
932
|
} catch (firebaseError) {
|
|
931
933
|
console.error("[AUTH] Firebase user creation failed:", firebaseError);
|
|
932
|
-
throw firebaseError;
|
|
934
|
+
throw handleFirebaseError(firebaseError);
|
|
933
935
|
}
|
|
934
936
|
|
|
935
|
-
//
|
|
936
|
-
console.log("[AUTH]
|
|
937
|
-
|
|
938
|
-
try {
|
|
939
|
-
user = await this.userService.createUser(
|
|
940
|
-
firebaseUser,
|
|
941
|
-
[UserRole.PRACTITIONER],
|
|
942
|
-
{
|
|
943
|
-
skipProfileCreation: true, // We'll create the profile separately
|
|
944
|
-
}
|
|
945
|
-
);
|
|
946
|
-
console.log("[AUTH] User with PRACTITIONER role created successfully", {
|
|
947
|
-
userId: user.uid,
|
|
948
|
-
});
|
|
949
|
-
} catch (userCreationError) {
|
|
950
|
-
console.error("[AUTH] User creation failed:", userCreationError);
|
|
951
|
-
throw userCreationError;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Initialize practitioner service
|
|
955
|
-
console.log("[AUTH] Initializing practitioner service");
|
|
956
|
-
const practitionerService = new PractitionerService(
|
|
937
|
+
// Step 3: Execute all database operations in a single transaction
|
|
938
|
+
console.log("[AUTH] Starting Firestore transaction");
|
|
939
|
+
const transactionResult = await runTransaction(
|
|
957
940
|
this.db,
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
let practitioner: Practitioner | null = null;
|
|
941
|
+
async (transaction) => {
|
|
942
|
+
console.log(
|
|
943
|
+
"[AUTH] Transaction started - creating user and practitioner"
|
|
944
|
+
);
|
|
963
945
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
946
|
+
// Initialize services
|
|
947
|
+
const practitionerService = new PractitionerService(
|
|
948
|
+
this.db,
|
|
949
|
+
this.auth,
|
|
950
|
+
this.app
|
|
951
|
+
);
|
|
967
952
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
953
|
+
// Create user document using existing method (not in transaction for now)
|
|
954
|
+
console.log("[AUTH] Creating user document");
|
|
955
|
+
const user = await this.userService.createUser(
|
|
956
|
+
firebaseUser,
|
|
957
|
+
[UserRole.PRACTITIONER],
|
|
958
|
+
{ skipProfileCreation: true }
|
|
973
959
|
);
|
|
974
960
|
|
|
975
|
-
|
|
976
|
-
|
|
961
|
+
let practitioner: Practitioner;
|
|
962
|
+
|
|
963
|
+
// Handle practitioner profile creation/claiming
|
|
964
|
+
if (data.token) {
|
|
965
|
+
console.log(
|
|
966
|
+
"[AUTH] Claiming existing practitioner profile with token"
|
|
967
|
+
);
|
|
968
|
+
const claimedPractitioner =
|
|
969
|
+
await practitionerService.validateTokenAndClaimProfile(
|
|
970
|
+
data.token,
|
|
971
|
+
firebaseUser.uid
|
|
972
|
+
);
|
|
973
|
+
if (!claimedPractitioner) {
|
|
974
|
+
throw new Error("Invalid or expired invitation token");
|
|
975
|
+
}
|
|
976
|
+
practitioner = claimedPractitioner;
|
|
977
|
+
} else {
|
|
978
|
+
console.log("[AUTH] Creating new practitioner profile");
|
|
979
|
+
const practitionerData = buildPractitionerData(
|
|
980
|
+
data,
|
|
981
|
+
firebaseUser.uid
|
|
982
|
+
);
|
|
983
|
+
practitioner = await practitionerService.createPractitioner(
|
|
984
|
+
practitionerData
|
|
985
|
+
);
|
|
977
986
|
}
|
|
978
987
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
// Link the practitioner profile to the user
|
|
988
|
+
// Link practitioner to user
|
|
989
|
+
console.log("[AUTH] Linking practitioner to user");
|
|
984
990
|
await this.userService.updateUser(firebaseUser.uid, {
|
|
985
991
|
practitionerProfile: practitioner.id,
|
|
986
992
|
});
|
|
987
|
-
console.log(
|
|
988
|
-
"[AUTH] User updated with practitioner profile reference"
|
|
989
|
-
);
|
|
990
|
-
} catch (tokenError) {
|
|
991
|
-
console.error("[AUTH] Failed to claim draft profile:", tokenError);
|
|
992
|
-
throw tokenError;
|
|
993
|
-
}
|
|
994
|
-
} else {
|
|
995
|
-
console.log("[AUTH] Creating new practitioner profile");
|
|
996
993
|
|
|
997
|
-
|
|
998
|
-
|
|
994
|
+
console.log("[AUTH] Transaction operations completed successfully");
|
|
995
|
+
return { user, practitioner };
|
|
999
996
|
}
|
|
997
|
+
);
|
|
1000
998
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
email: data.email,
|
|
1006
|
-
phoneNumber: data.profileData.basicInfo?.phoneNumber || "",
|
|
1007
|
-
profileImageUrl: data.profileData.basicInfo?.profileImageUrl || "",
|
|
1008
|
-
gender: data.profileData.basicInfo?.gender || "other", // Default to "other" if not provided
|
|
1009
|
-
bio: data.profileData.basicInfo?.bio || "",
|
|
1010
|
-
title: "Practitioner", // Default title
|
|
1011
|
-
dateOfBirth: new Date(), // Default to today
|
|
1012
|
-
languages: ["English"], // Default language
|
|
1013
|
-
};
|
|
999
|
+
console.log("[AUTH] Atomic practitioner signup completed successfully", {
|
|
1000
|
+
userId: transactionResult.user.uid,
|
|
1001
|
+
practitionerId: transactionResult.practitioner.id,
|
|
1002
|
+
});
|
|
1014
1003
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
issuingAuthority: "Pending",
|
|
1022
|
-
issueDate: new Date(),
|
|
1023
|
-
verificationStatus: "pending",
|
|
1024
|
-
};
|
|
1004
|
+
return transactionResult;
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
console.error(
|
|
1007
|
+
"[AUTH] Atomic signup failed, initiating cleanup...",
|
|
1008
|
+
error
|
|
1009
|
+
);
|
|
1025
1010
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
certification,
|
|
1031
|
-
status: PractitionerStatus.ACTIVE,
|
|
1032
|
-
isActive: true,
|
|
1033
|
-
isVerified: false,
|
|
1034
|
-
};
|
|
1011
|
+
// Cleanup Firebase user if transaction failed
|
|
1012
|
+
if (firebaseUser) {
|
|
1013
|
+
await cleanupFirebaseUser(firebaseUser);
|
|
1014
|
+
}
|
|
1035
1015
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
);
|
|
1040
|
-
console.log("[AUTH] Practitioner profile created successfully", {
|
|
1041
|
-
practitionerId: practitioner.id,
|
|
1042
|
-
});
|
|
1016
|
+
throw handleSignupError(error);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1043
1019
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Pre-validate all signup data before any mutations
|
|
1022
|
+
* Prevents partial creation by catching issues early
|
|
1023
|
+
*/
|
|
1024
|
+
private async validateSignupData(data: {
|
|
1025
|
+
email: string;
|
|
1026
|
+
password: string;
|
|
1027
|
+
firstName?: string;
|
|
1028
|
+
lastName?: string;
|
|
1029
|
+
token?: string;
|
|
1030
|
+
profileData?: Partial<CreatePractitionerData>;
|
|
1031
|
+
}): Promise<void> {
|
|
1032
|
+
console.log("[AUTH] Pre-validating signup data");
|
|
1059
1033
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1034
|
+
try {
|
|
1035
|
+
// 1. Schema validation
|
|
1036
|
+
await practitionerSignupSchema.parseAsync(data);
|
|
1037
|
+
console.log("[AUTH] Schema validation passed");
|
|
1038
|
+
|
|
1039
|
+
// 2. Check if email already exists (before creating Firebase user)
|
|
1040
|
+
const emailExists = await checkEmailExists(this.auth, data.email);
|
|
1041
|
+
if (emailExists) {
|
|
1042
|
+
console.log("[AUTH] Email already exists:", data.email);
|
|
1043
|
+
throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
|
|
1044
|
+
}
|
|
1045
|
+
console.log("[AUTH] Email availability confirmed");
|
|
1064
1046
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
console.error(
|
|
1072
|
-
"[AUTH] Zod validation error in signUpPractitioner:",
|
|
1073
|
-
JSON.stringify(error.errors, null, 2)
|
|
1047
|
+
// 3. Validate token if provided
|
|
1048
|
+
if (data.token) {
|
|
1049
|
+
const practitionerService = new PractitionerService(
|
|
1050
|
+
this.db,
|
|
1051
|
+
this.auth,
|
|
1052
|
+
this.app
|
|
1074
1053
|
);
|
|
1075
|
-
|
|
1054
|
+
const isValidToken = await practitionerService.validateToken(
|
|
1055
|
+
data.token
|
|
1056
|
+
);
|
|
1057
|
+
if (!isValidToken) {
|
|
1058
|
+
console.log("[AUTH] Invalid token provided:", data.token);
|
|
1059
|
+
throw new Error("Invalid or expired invitation token");
|
|
1060
|
+
}
|
|
1061
|
+
console.log("[AUTH] Token validation passed");
|
|
1076
1062
|
}
|
|
1077
1063
|
|
|
1078
|
-
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1064
|
+
// 4. Validate profile data structure if provided
|
|
1065
|
+
if (data.profileData) {
|
|
1066
|
+
await validatePractitionerProfileData(data.profileData);
|
|
1067
|
+
console.log("[AUTH] Profile data validation passed");
|
|
1082
1068
|
}
|
|
1083
1069
|
|
|
1084
|
-
console.
|
|
1070
|
+
console.log("[AUTH] All pre-validation checks passed");
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.error("[AUTH] Pre-validation failed:", error);
|
|
1085
1073
|
throw error;
|
|
1086
1074
|
}
|
|
1087
1075
|
}
|
|
@@ -110,7 +110,7 @@ export class PractitionerService extends BaseService {
|
|
|
110
110
|
* @returns URL string of the uploaded or existing photo
|
|
111
111
|
*/
|
|
112
112
|
private async handleProfilePhotoUpload(
|
|
113
|
-
profilePhoto: MediaResource | undefined,
|
|
113
|
+
profilePhoto: MediaResource | undefined | null,
|
|
114
114
|
practitionerId: string
|
|
115
115
|
): Promise<string | undefined> {
|
|
116
116
|
if (!profilePhoto) {
|
|
@@ -151,7 +151,9 @@ export class PractitionerService extends BaseService {
|
|
|
151
151
|
* @returns Processed basic info with URL string for profileImageUrl
|
|
152
152
|
*/
|
|
153
153
|
private async processBasicInfo(
|
|
154
|
-
basicInfo: PractitionerBasicInfo & {
|
|
154
|
+
basicInfo: PractitionerBasicInfo & {
|
|
155
|
+
profileImageUrl?: MediaResource | null;
|
|
156
|
+
},
|
|
155
157
|
practitionerId: string
|
|
156
158
|
): Promise<PractitionerBasicInfo> {
|
|
157
159
|
const processedBasicInfo = { ...basicInfo };
|
|
@@ -723,7 +725,7 @@ export class PractitionerService extends BaseService {
|
|
|
723
725
|
if (validData.basicInfo) {
|
|
724
726
|
processedData.basicInfo = await this.processBasicInfo(
|
|
725
727
|
validData.basicInfo as PractitionerBasicInfo & {
|
|
726
|
-
profileImageUrl?: MediaResource;
|
|
728
|
+
profileImageUrl?: MediaResource | null;
|
|
727
729
|
},
|
|
728
730
|
practitionerId
|
|
729
731
|
);
|
|
@@ -24,10 +24,10 @@ export interface PractitionerBasicInfo {
|
|
|
24
24
|
lastName: string;
|
|
25
25
|
title: string;
|
|
26
26
|
email: string;
|
|
27
|
-
phoneNumber: string;
|
|
28
|
-
dateOfBirth: Timestamp | Date;
|
|
27
|
+
phoneNumber: string | null;
|
|
28
|
+
dateOfBirth: Timestamp | Date | null;
|
|
29
29
|
gender: "male" | "female" | "other";
|
|
30
|
-
profileImageUrl?: MediaResource;
|
|
30
|
+
profileImageUrl?: MediaResource | null;
|
|
31
31
|
bio?: string;
|
|
32
32
|
languages: string[];
|
|
33
33
|
}
|
|
@@ -41,7 +41,7 @@ export interface PractitionerCertification {
|
|
|
41
41
|
licenseNumber: string;
|
|
42
42
|
issuingAuthority: string;
|
|
43
43
|
issueDate: Timestamp | Date;
|
|
44
|
-
expiryDate?: Timestamp | Date;
|
|
44
|
+
expiryDate?: Timestamp | Date | null;
|
|
45
45
|
verificationStatus: "pending" | "verified" | "rejected";
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -25,10 +25,13 @@ export const practitionerBasicInfoSchema = z.object({
|
|
|
25
25
|
lastName: z.string().min(2).max(50),
|
|
26
26
|
title: z.string().min(2).max(100),
|
|
27
27
|
email: z.string().email(),
|
|
28
|
-
phoneNumber: z
|
|
29
|
-
|
|
28
|
+
phoneNumber: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number")
|
|
31
|
+
.nullable(),
|
|
32
|
+
dateOfBirth: z.instanceof(Timestamp).or(z.date()).nullable(),
|
|
30
33
|
gender: z.enum(["male", "female", "other"]),
|
|
31
|
-
profileImageUrl: mediaResourceSchema.optional(),
|
|
34
|
+
profileImageUrl: mediaResourceSchema.optional().nullable(),
|
|
32
35
|
bio: z.string().max(1000).optional(),
|
|
33
36
|
languages: z.array(z.string()).min(1),
|
|
34
37
|
});
|
|
@@ -42,7 +45,7 @@ export const practitionerCertificationSchema = z.object({
|
|
|
42
45
|
licenseNumber: z.string().min(3).max(50),
|
|
43
46
|
issuingAuthority: z.string().min(2).max(100),
|
|
44
47
|
issueDate: z.instanceof(Timestamp).or(z.date()),
|
|
45
|
-
expiryDate: z.instanceof(Timestamp).or(z.date()).optional(),
|
|
48
|
+
expiryDate: z.instanceof(Timestamp).or(z.date()).optional().nullable(),
|
|
46
49
|
verificationStatus: z.enum(["pending", "verified", "rejected"]),
|
|
47
50
|
});
|
|
48
51
|
|