Yesterday I encountered a very peculiar bug which I have not been able to solve. I essentially created services to handle user authentication and data management. I also created an additional service to handle cookies for data persistence. The issue begins when a Rout Guard executes the cookie service.
This is mainly because the cookie service uses document.cookies as this is what chatgpt suggested. I really didn’t consider this to be appropriate, knowing this was a potential issue. I also didn’t like the ngx-cookie-service approach to this solution. That’s why I opted to do it myself. When I execute the code, everything works as expected. I’m able to log in, logout and move around the page. The guard does its job as expected. However, I get an error saying the document isn’t available in the Guard’s context. Here is the error code
As you can see the error comes from the cookie service. Specifically from the following code (not full code just the relevant part)
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CookieService {
constructor() { }
/**
* Gets an object like cookie
* @param name name of the cookie to get
* @returns the value of the cookie requested or undefined
*/
public getCookie(name:string):object|undefined{
if(document!==undefined){
const cookies = document.cookie.split("; ")
for (const cookie of cookies){
const [key,value] = cookie.split("=")
if (key === name){
try{
let values = JSON.parse(decodeURIComponent(value));
console.log("Values", values)
return values;
}catch(error){
console.error('Error parsing cookie value:', error);
return undefined;
}
}
}
console.log(`Cookie with name "${name}" not found.`);
return undefined;
}else{
console.error("DOM not found")
return undefined;
}
}
Next I will provide the Guard code and the available Routs
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const firebaseService = inject(FirebaseService);
const router = inject(Router);
if(firebaseService.haveUser()){
return true;
}else{
return router.parseUrl('/');
}
};
export const sessionGuard: CanActivateFn = (routes:ActivatedRouteSnapshot, state:RouterStateSnapshot)=>{
const firebaseService = inject(FirebaseService);
const router = inject(Router);
if(!firebaseService.haveUser()){
return true;
}else{
return router.parseUrl('/Oportunidades');
}
}
Now the routes
export const routes: Routes = [
{path:'', redirectTo:"/IniciarSession",pathMatch:'full'},
{path:'IniciarSession',component:LoginComponent,canActivate:[sessionGuard]},
{path:'Oportunidades',component:OportunidadesComponent, canActivate:[authGuard]},
]
In order to see the use of the cookie service you will need to see part of the firebase service
@Injectable({
providedIn: 'root'
})
export class FirebaseService {
private app:FirebaseApp;
protected db: Firestore;
private auth:Auth;
protected userCredential?:UserCredential;
constructor(private cookie_service:CookieService) {
this.app = initializeApp(environment.firebaseConfig);
this.db = getFirestore(this.app);
this.auth = getAuth(this.app);
}
//Falta agregar que revise si la sesion sigue activa
public haveUser():boolean{
this.checkPersistance()
if(this.userCredential){
return true;
}else{
return false;
}
}
private checkPersistance():void{
const credentials = this.cookie_service.getCookie("userCredentials")
if(credentials){
this.userCredential=credentials as UserCredential;
}else{
console.log("No user persisted")
}
}
}
I ignored the error for a while because the code just works. I don’t know why it just does. Of course, I knew this would cause problems later on and not soon after I stumbled with this nasty bug. Where I do get the expected data, and suddenly it changes and gives me a weird promise. This happens when I’m saving a cookie using the profile service, which is dedicated to handling only profile data. Which is then called by the login component to save it into a cookie. Which latter the cookie gets called and the error begins.
@Injectable({
providedIn: 'root'
})
export class ProfileService extends FirebaseService {
constructor(cookie_service:CookieService) {
super(cookie_service);
}
public async getUserProfile():Promise<DocumentData|null|undefined>{
if(this.userCredential){
const usersCollection = collection(this.db,"users");
const userDoc = doc(usersCollection,this.userCredential.user.uid);
const userDocSnap = await getDoc(userDoc)
if(userDocSnap.exists()){
return userDocSnap.data();
}else{
return null
}
}else{
return null
}
}
}
This is called in the login component, which then redirects you to the starting page. And on the starting page is where we try to access the user profile cookie. Which does work at first but the suddenly changes value, and I’m unable to use it.
@Component({
selector: 'app-login',
standalone: true,
imports: [MagicInputComponent,ReactiveFormsModule,ToasterComponent],
templateUrl: './login.component.html',
})
export class LoginComponent {
loginForm = new FormGroup({
username: new FormControl(''),
password: new FormControl('')
});
@ViewChild(ToasterComponent) ToasterTools!:ToasterComponent;
constructor(private router:Router, private auth:FirebaseService, private profile: ProfileService, private cookie_service:CookieService){}
async onSubmit(){
let username = this.loginForm.get("username")!.value as string;
let password = this.loginForm.get("password")!.value as string;
try{
await this.auth.Login(username,password);
this.ToasterTools.setKind(ToastKind.success);
this.ToasterTools.setTitle("Inicio de Session exitoso");
this.ToasterTools.setMessage("");
this.ToasterTools.showToast();
const user_profile = this.profile.getUserProfile(); //<--- get the user profile from firebase
if(user_profile!=null){
await this.cookie_service.setCookie("user_profile", user_profile,undefined ,1); //<-- Here we set the user_profile cookie
}else{
this.ToasterTools.setKind(ToastKind.error);
this.ToasterTools.setTitle("Error");
this.ToasterTools.setMessage("La carga de tu perfil fallo, intentelo denuevo mas tarde");
this.ToasterTools.showToast();
return;
}
this.router.navigate(["/Oportunidades"]);
}catch(error){
this.ToasterTools.setKind(ToastKind.error);
this.ToasterTools.setTitle("Error");
this.ToasterTools.setMessage("Usuario o contraseña incorrectos");
this.ToasterTools.showToast();
console.error(error);
}
}
}
Here is the sarting page component
@Component({
selector: 'app-oportunidades',
standalone: true,
imports: [UserDataHeaderComponent,PropertyCardComponent],
templateUrl: './oportunidades.component.html',
styleUrl: './oportunidades.component.css'
})
export class OportunidadesComponent implements OnInit {
cards = [1,2,3,4]
constructor(private cookie_service:CookieService){}
ngOnInit(): void {
const value = this.cookie_service.getCookie("user_profile")
console.log("Client value",value)
}
}
In the following image, you are seeing the output of the console logs of the cookie. As you can see, the value from the getCookies of the cookie service just suddenly changes value without me being able to use the original value which is the correct one. Then the client receives the incorrect one.