fix login redirect

This commit is contained in:
2026-03-15 22:54:52 +01:00
parent 2406d41dcb
commit e3203d0c38
8 changed files with 110 additions and 28 deletions

View File

@@ -53,9 +53,9 @@ export class InfoComponent implements OnInit {
await this.userService.update$(uid, {chordMode: value}); await this.userService.update$(uid, {chordMode: value});
} }
public getUserRoles = (roles: string): roles[] => (roles?.split(';') ?? []) as roles[]; public getUserRoles = (role: string): roles[] => (role?.split(';') ?? []) as roles[];
public transdormUserRoles = (roles: roles): string => public transdormUserRoles = (role: string): string =>
this.getUserRoles(roles) this.getUserRoles(role)
.map(_ => new RolePipe().transform(_)) .map(_ => new RolePipe().transform(_))
.join(', '); .join(', ');
} }

View File

@@ -3,7 +3,7 @@ import {roles} from '../../../services/user/roles';
@Pipe({name: 'role'}) @Pipe({name: 'role'})
export class RolePipe implements PipeTransform { export class RolePipe implements PipeTransform {
public transform(role: roles): string { public transform(role: roles | string): string {
switch (role) { switch (role) {
case 'contributor': case 'contributor':
return 'Mitarbeiter'; return 'Mitarbeiter';

View File

@@ -1,7 +1,7 @@
import {Component, Input, inject} from '@angular/core'; import {Component, Input, inject} from '@angular/core';
import {User} from '../../../../../services/user/user'; import {User} from '../../../../../services/user/user';
import {UserService} from '../../../../../services/user/user.service'; import {UserService} from '../../../../../services/user/user.service';
import {ROLE_TYPES} from '../../../../../services/user/roles'; import {ROLE_TYPES, roles} from '../../../../../services/user/roles';
import {faTimes} from '@fortawesome/free-solid-svg-icons'; import {faTimes} from '@fortawesome/free-solid-svg-icons';
import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatFormField, MatLabel} from '@angular/material/form-field';
@@ -24,7 +24,7 @@ export class UserComponent {
public id = ''; public id = '';
public name = ''; public name = '';
public roles: string[] = []; public roles: roles[] = [];
public ROLE_TYPES = ROLE_TYPES; public ROLE_TYPES = ROLE_TYPES;
public edit = false; public edit = false;
public faClose = faTimes; public faClose = faTimes;
@@ -36,7 +36,7 @@ export class UserComponent {
this.roles = this.getRoleArray(value.role); this.roles = this.getRoleArray(value.role);
} }
public async onRoleChanged(id: string, roles: string[]): Promise<void> { public async onRoleChanged(id: string, roles: roles[]): Promise<void> {
const role = roles.join(';'); const role = roles.join(';');
await this.userService.update$(id, {role}); await this.userService.update$(id, {role});
} }
@@ -46,7 +46,7 @@ export class UserComponent {
await this.userService.update$(id, {name: target.value}); await this.userService.update$(id, {name: target.value});
} }
public getRoleArray(role: string): string[] { public getRoleArray(role: string): roles[] {
return role ? role.split(';') : []; return (role ? role.split(';') : []) as roles[];
} }
} }

View File

@@ -85,6 +85,7 @@ describe('UserSessionService', () => {
const updateSpy = jasmine.createSpy('update').and.resolveTo(); const updateSpy = jasmine.createSpy('update').and.resolveTo();
dbServiceSpy.doc.and.returnValue({update: updateSpy} as never); dbServiceSpy.doc.and.returnValue({update: updateSpy} as never);
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}}); runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
authStateSubject.next({uid: 'user-1'});
await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1'); await expectAsync(service.login('mail', 'secret')).toBeResolvedTo('user-1');
@@ -92,6 +93,23 @@ describe('UserSessionService', () => {
expect(updateSpy).toHaveBeenCalledWith({songUsage: {}}); expect(updateSpy).toHaveBeenCalledWith({songUsage: {}});
}); });
it('should wait for auth state propagation before resolving login', async () => {
runInFirebaseContextSpy.and.resolveTo({user: {uid: 'user-1'}});
let resolved = false;
const loginPromise = service.login('mail', 'secret').then(result => {
resolved = true;
return result;
});
await Promise.resolve();
expect(resolved).toBeFalse();
authStateSubject.next({uid: 'user-1'});
await expectAsync(loginPromise).toBeResolvedTo('user-1');
});
it('should delegate logout and password reset to AngularFire auth APIs', async () => { it('should delegate logout and password reset to AngularFire auth APIs', async () => {
runInFirebaseContextSpy.and.resolveTo(); runInFirebaseContextSpy.and.resolveTo();

View File

@@ -2,7 +2,7 @@ import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@a
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth'; import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
import {User as AuthUser} from 'firebase/auth'; import {User as AuthUser} from 'firebase/auth';
import {firstValueFrom, Observable, of} from 'rxjs'; import {firstValueFrom, Observable, of} from 'rxjs';
import {map, shareReplay, switchMap} from 'rxjs/operators'; import {filter, map, shareReplay, switchMap, take} from 'rxjs/operators';
import {DbService} from '../db.service'; import {DbService} from '../db.service';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
@@ -59,6 +59,7 @@ export class UserSessionService {
const dUser = await this.readUser(aUser.user.uid); const dUser = await this.readUser(aUser.user.uid);
if (!dUser) return null; if (!dUser) return null;
await this.initSongUsage(dUser); await this.initSongUsage(dUser);
await this.awaitAuthenticatedUser(aUser.user.uid);
return aUser.user.uid; return aUser.user.uid;
} }
@@ -90,6 +91,15 @@ export class UserSessionService {
await this.update$(user.id, {songUsage: {}}); await this.update$(user.id, {songUsage: {}});
} }
private async awaitAuthenticatedUser(uid: string): Promise<void> {
await firstValueFrom(
this.user$.pipe(
filter((user): user is User => !!user && user.id === uid),
take(1)
)
);
}
private runInFirebaseContext = <T>(factory: () => T): T => runInInjectionContext(this.environmentInjector, factory); private runInFirebaseContext = <T>(factory: () => T): T => runInInjectionContext(this.environmentInjector, factory);
private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid); private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid);
private readUser = (uid: string): Promise<User | null> => firstValueFrom(this.readUser$(uid)); private readUser = (uid: string): Promise<User | null> => firstValueFrom(this.readUser$(uid));

View File

@@ -89,4 +89,42 @@ describe('RoleGuard', () => {
done(); done();
}); });
}); });
it('should redirect members to shows instead of new-user', done => {
TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
routerSpy.createUrlTree.and.returnValue({redirect: ['shows']} as never);
TestBed.configureTestingModule({
providers: [
{provide: Router, useValue: routerSpy},
{provide: UserService, useValue: {user$: of({role: 'member'})}},
],
});
guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['user']}} as never).subscribe(result => {
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
expect(result).toEqual({redirect: ['shows']} as never);
done();
});
});
it('should choose a matching default route from all assigned roles', done => {
TestBed.resetTestingModule();
routerSpy = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);
routerSpy.createUrlTree.and.callFake(commands => ({redirect: commands}) as never);
TestBed.configureTestingModule({
providers: [
{provide: Router, useValue: routerSpy},
{provide: UserService, useValue: {user$: of({role: ' none ; leader '})}},
],
});
guard = TestBed.inject(RoleGuard);
guard.canActivate({data: {requiredRoles: ['presenter']}} as never).subscribe(result => {
expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['shows']);
expect(result).toEqual({redirect: ['shows']} as never);
done();
});
});
}); });

View File

@@ -3,6 +3,7 @@ import {ActivatedRouteSnapshot, Router, UrlTree} from '@angular/router';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import {UserService} from '../../services/user/user.service'; import {UserService} from '../../services/user/user.service';
import {map, take} from 'rxjs/operators'; import {map, take} from 'rxjs/operators';
import {roles} from '../../services/user/roles';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -23,30 +24,45 @@ export class RoleGuard {
if (!user) { if (!user) {
return this.router.createUrlTree(['brand', 'new-user']); return this.router.createUrlTree(['brand', 'new-user']);
} }
const roles = user.role?.split(';') ?? []; const userRoles = this.parseRoles(user.role);
if (roles.indexOf('admin') !== -1) { if (userRoles.includes('admin')) {
return true; return true;
} }
const allowed = roles.some(s => requiredRoles.indexOf(s) !== -1); const allowed = userRoles.some(role => requiredRoles.includes(role));
return allowed || this.router.createUrlTree(this.defaultRoute(roles)); return allowed || this.router.createUrlTree(this.defaultRoute(userRoles));
}) })
); );
} }
private defaultRoute(roles: string[]): string[] { private defaultRoute(userRoles: roles[]): string[] {
if (!roles || roles.length === 0) { if (userRoles.length === 0) {
return ['brand', 'new-user']; return ['brand', 'new-user'];
} }
switch (roles[0]) {
case 'user': if (userRoles.includes('user') || userRoles.includes('contributor')) {
return ['songs']; return ['songs'];
case 'presenter':
return ['presentation'];
case 'leader':
return ['shows'];
} }
return ['brand', 'new-user']; if (userRoles.includes('leader') || userRoles.includes('member')) {
return ['shows'];
}
if (userRoles.includes('presenter')) {
return ['presentation'];
}
return ['user', 'info'];
}
private parseRoles(role: string | null | undefined): roles[] {
if (!role) {
return [];
}
return role
.split(';')
.map(value => value.trim())
.filter((value): value is roles => value.length > 0 && value !== 'none');
} }
} }

View File

@@ -4,15 +4,15 @@ import {Pipe, PipeTransform} from '@angular/core';
name: 'sortBy', name: 'sortBy',
}) })
export class SortByPipe implements PipeTransform { export class SortByPipe implements PipeTransform {
public transform(value: unknown[] | null, order: 'asc' | 'desc' = 'asc', column = ''): unknown[] | null { public transform<T>(value: T[] | null, order: 'asc' | 'desc' = 'asc', column = ''): T[] | null {
if (!value || !order) { if (!value || !order) {
return value; return value;
} // no array } // no array
if (!column || column === '') { if (!column || column === '') {
if (order === 'asc') { if (order === 'asc') {
return value.sort(); return [...value].sort();
} else { } else {
return value.sort().reverse(); return [...value].sort().reverse();
} }
} // sort 1d array } // sort 1d array
if (value.length <= 1) { if (value.length <= 1) {
@@ -26,7 +26,7 @@ export class SortByPipe implements PipeTransform {
}); });
} }
private getComparableValue(item: unknown, column: string): string | number { private getComparableValue<T>(item: T, column: string): string | number {
const value = (item as Record<string, unknown>)[column]; const value = (item as Record<string, unknown>)[column];
if (value instanceof Date) { if (value instanceof Date) {
return value.getTime(); return value.getTime();