fix user serivce
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {ShowDataService} from './show-data.service';
|
||||
import {Show} from './show';
|
||||
import {Observable} from 'rxjs';
|
||||
import {firstValueFrom, Observable} from 'rxjs';
|
||||
import {UserService} from '../../../services/user/user.service';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
import {User} from '../../../services/user/user';
|
||||
@@ -16,13 +16,6 @@ export class ShowService {
|
||||
public static SHOW_TYPE = ['service-worship', 'service-praise', 'home-group-big', 'home-group', 'prayer-group', 'teens-group', 'kids-group', 'misc-public', 'misc-private'];
|
||||
public static SHOW_TYPE_PUBLIC = ['service-worship', 'service-praise', 'home-group-big', 'teens-group', 'kids-group', 'misc-public'];
|
||||
public static SHOW_TYPE_PRIVATE = ['home-group', 'prayer-group', 'misc-private'];
|
||||
private user: User | null = null;
|
||||
|
||||
public constructor() {
|
||||
const userService = this.userService;
|
||||
|
||||
userService.user$.subscribe(_ => (this.user = _));
|
||||
}
|
||||
|
||||
public read$ = (showId: string): Observable<Show | null> => this.showDataService.read$(showId);
|
||||
public listPublicSince$ = (lastMonths: number): Observable<Show[]> => this.showDataService.listPublicSince$(lastMonths);
|
||||
@@ -40,10 +33,11 @@ export class ShowService {
|
||||
public update$ = async (showId: string, data: Partial<Show>): Promise<void> => this.showDataService.update(showId, data);
|
||||
|
||||
public async new$(data: Partial<Show>): Promise<string | null> {
|
||||
if (!data.showType || !this.user) return null;
|
||||
const user = await firstValueFrom(this.userService.user$);
|
||||
if (!data.showType || !user) return null;
|
||||
const calculatedData: Partial<Show> = {
|
||||
...data,
|
||||
owner: this.user.id,
|
||||
owner: user.id,
|
||||
order: [],
|
||||
public: ShowService.SHOW_TYPE_PUBLIC.indexOf(data.showType) !== -1,
|
||||
};
|
||||
|
||||
99
src/app/services/user/user-session.service.ts
Normal file
99
src/app/services/user/user-session.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
|
||||
import {User as AuthUser} from 'firebase/auth';
|
||||
import {firstValueFrom, Observable, of} from 'rxjs';
|
||||
import {map, shareReplay, switchMap} from 'rxjs/operators';
|
||||
import {DbService} from '../db.service';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {Router} from '@angular/router';
|
||||
import {User} from './user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserSessionService {
|
||||
private auth = inject(Auth);
|
||||
private db = inject(DbService);
|
||||
private router = inject(Router);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
private authStateChanges$ = this.createAuthState$().pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||
public users$ = this.db.col$<User>('users').pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||
private userByIdCache = new Map<string, Observable<User | null>>();
|
||||
private iUserId$ = this.authStateChanges$.pipe(
|
||||
map(auth => auth?.uid ?? null),
|
||||
shareReplay({bufferSize: 1, refCount: true})
|
||||
);
|
||||
private iUser$ = this.iUserId$.pipe(
|
||||
switchMap(uid => (uid ? this.readUser$(uid) : of(null))),
|
||||
shareReplay({bufferSize: 1, refCount: true})
|
||||
);
|
||||
|
||||
public get userId$(): Observable<string | null> {
|
||||
return this.iUserId$;
|
||||
}
|
||||
|
||||
public get user$(): Observable<User | null> {
|
||||
return this.iUser$;
|
||||
}
|
||||
|
||||
public currentUser = async (): Promise<User | null> => firstValueFrom(this.user$);
|
||||
public loggedIn$ = (): Observable<boolean> => this.authStateChanges$.pipe(map(auth => !!auth));
|
||||
public list$ = (): Observable<User[]> => this.users$;
|
||||
|
||||
public getUserbyId = (userId: string): Promise<User | null> => firstValueFrom(this.getUserbyId$(userId));
|
||||
public getUserbyId$ = (userId: string): Observable<User | null> => {
|
||||
const cached = this.userByIdCache.get(userId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const user$ = this.db.doc$<User>(`users/${userId}`).pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||
this.userByIdCache.set(userId, user$);
|
||||
return user$;
|
||||
};
|
||||
|
||||
public async login(user: string, password: string): Promise<string | null> {
|
||||
const aUser = await this.runInFirebaseContext(() => signInWithEmailAndPassword(this.auth, user, password));
|
||||
if (!aUser.user) return null;
|
||||
const dUser = await this.readUser(aUser.user.uid);
|
||||
if (!dUser) return null;
|
||||
await this.initSongUsage(dUser);
|
||||
|
||||
return aUser.user.uid;
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
await this.runInFirebaseContext(() => signOut(this.auth));
|
||||
}
|
||||
|
||||
public async update$(uid: string, data: Partial<User>): Promise<void> {
|
||||
await this.db.doc<User>('users/' + uid).update(data);
|
||||
}
|
||||
|
||||
public async changePassword(user: string): Promise<void> {
|
||||
await this.runInFirebaseContext(() => sendPasswordResetEmail(this.auth, user, {url: environment.url}));
|
||||
}
|
||||
|
||||
public async createNewUser(user: string, name: string, password: string): Promise<void> {
|
||||
const aUser = await this.runInFirebaseContext(() => createUserWithEmailAndPassword(this.auth, user, password));
|
||||
if (!aUser.user) return;
|
||||
const userId = aUser.user.uid;
|
||||
await this.db.doc('users/' + userId).set({name, chordMode: 'onlyFirst', songUsage: {}});
|
||||
const dUser = await this.readUser(aUser.user.uid);
|
||||
if (!dUser) return;
|
||||
await this.router.navigateByUrl('/brand/new-user');
|
||||
}
|
||||
|
||||
private async initSongUsage(user: User) {
|
||||
if (user.songUsage) return;
|
||||
await this.update$(user.id, {songUsage: {}});
|
||||
}
|
||||
|
||||
private runInFirebaseContext = <T>(factory: () => T): T => runInInjectionContext(this.environmentInjector, factory);
|
||||
private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid);
|
||||
private readUser = (uid: string): Promise<User | null> => firstValueFrom(this.readUser$(uid));
|
||||
private createAuthState$(): Observable<AuthUser | null> {
|
||||
return runInInjectionContext(this.environmentInjector, () => authState(this.auth));
|
||||
}
|
||||
}
|
||||
89
src/app/services/user/user-song-usage.service.ts
Normal file
89
src/app/services/user/user-song-usage.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {firstValueFrom} from 'rxjs';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {increment} from '@angular/fire/firestore';
|
||||
import {DbService} from '../db.service';
|
||||
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
||||
import {ShowSongDataService} from '../../modules/shows/services/show-song-data.service';
|
||||
import {UserSessionService} from './user-session.service';
|
||||
|
||||
export interface SongUsageMigrationResult {
|
||||
usersProcessed: number;
|
||||
showsProcessed: number;
|
||||
showSongsProcessed: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserSongUsageService {
|
||||
private db = inject(DbService);
|
||||
private session = inject(UserSessionService);
|
||||
private showDataService = inject(ShowDataService);
|
||||
private showSongDataService = inject(ShowSongDataService);
|
||||
|
||||
public incSongCount = (songId: string) => this.updateSongUsage(songId, 1);
|
||||
public decSongCount = (songId: string) => this.updateSongUsage(songId, -1);
|
||||
|
||||
public async rebuildSongUsage(): Promise<SongUsageMigrationResult> {
|
||||
const currentUser = await firstValueFrom(this.session.user$.pipe(take(1)));
|
||||
if (!currentUser || !this.hasAdminRole(currentUser.role)) {
|
||||
throw new Error('Admin role required to rebuild songUsage.');
|
||||
}
|
||||
|
||||
const [users, shows] = await Promise.all([firstValueFrom(this.session.users$), firstValueFrom(this.showDataService.listRaw$())]);
|
||||
const songUsageByUserId: Record<string, Record<string, number>> = {};
|
||||
|
||||
users.forEach(user => {
|
||||
songUsageByUserId[user.id] = {};
|
||||
});
|
||||
|
||||
let showSongsProcessed = 0;
|
||||
for (const show of shows) {
|
||||
const ownerId = show.owner;
|
||||
if (!ownerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const showSongs = await firstValueFrom(this.showSongDataService.list$(show.id));
|
||||
const usage = songUsageByUserId[ownerId] ?? {};
|
||||
songUsageByUserId[ownerId] = usage;
|
||||
|
||||
for (const showSong of showSongs) {
|
||||
showSongsProcessed += 1;
|
||||
usage[showSong.songId] = (usage[showSong.songId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
users.map(user =>
|
||||
this.session.update$(user.id, {
|
||||
songUsage: songUsageByUserId[user.id] ?? {},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
usersProcessed: users.length,
|
||||
showsProcessed: shows.length,
|
||||
showSongsProcessed,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateSongUsage(songId: string, direction: number) {
|
||||
const user = await firstValueFrom(this.session.user$);
|
||||
if (!user) return null;
|
||||
|
||||
await this.db.doc('users/' + user.id).update({
|
||||
[`songUsage.${songId}`]: increment(direction),
|
||||
});
|
||||
}
|
||||
|
||||
private hasAdminRole(role: string | null | undefined): boolean {
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return role.split(';').includes('admin');
|
||||
}
|
||||
}
|
||||
@@ -1,184 +1,37 @@
|
||||
import {EnvironmentInjector, Injectable, inject, runInInjectionContext} from '@angular/core';
|
||||
import {Auth, authState, createUserWithEmailAndPassword, sendPasswordResetEmail, signInWithEmailAndPassword, signOut} from '@angular/fire/auth';
|
||||
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs';
|
||||
import {filter, map, shareReplay, switchMap, take, tap} from 'rxjs/operators';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {User} from './user';
|
||||
import {DbService} from '../db.service';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {Router} from '@angular/router';
|
||||
import {ShowDataService} from '../../modules/shows/services/show-data.service';
|
||||
import {ShowSongDataService} from '../../modules/shows/services/show-song-data.service';
|
||||
import {increment} from '@angular/fire/firestore';
|
||||
|
||||
export interface SongUsageMigrationResult {
|
||||
usersProcessed: number;
|
||||
showsProcessed: number;
|
||||
showSongsProcessed: number;
|
||||
}
|
||||
import {SongUsageMigrationResult, UserSongUsageService} from './user-song-usage.service';
|
||||
import {UserSessionService} from './user-session.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
private auth = inject(Auth);
|
||||
private db = inject(DbService);
|
||||
private router = inject(Router);
|
||||
private showDataService = inject(ShowDataService);
|
||||
private showSongDataService = inject(ShowSongDataService);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
private session = inject(UserSessionService);
|
||||
private songUsage = inject(UserSongUsageService);
|
||||
|
||||
public users$ = this.db.col$<User>('users').pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||
private iUserId$ = new BehaviorSubject<string | null>(null);
|
||||
private iUser$ = new BehaviorSubject<User | null>(null);
|
||||
private userByIdCache = new Map<string, Observable<User | null>>();
|
||||
|
||||
public constructor() {
|
||||
this.authState$()
|
||||
.pipe(
|
||||
filter(auth => !!auth),
|
||||
map(auth => auth?.uid ?? ''),
|
||||
tap(uid => this.iUserId$.next(uid)),
|
||||
switchMap(uid => this.readUser$(uid))
|
||||
)
|
||||
.subscribe(_ => this.iUser$.next(_));
|
||||
}
|
||||
public users$ = this.session.users$;
|
||||
|
||||
public get userId$(): Observable<string | null> {
|
||||
return this.iUserId$.asObservable();
|
||||
return this.session.userId$;
|
||||
}
|
||||
|
||||
public get user$(): Observable<User | null> {
|
||||
return this.iUser$.pipe(filter(_ => !!_));
|
||||
return this.session.user$;
|
||||
}
|
||||
|
||||
public currentUser = async (): Promise<User | null> => firstValueFrom(this.user$);
|
||||
|
||||
public getUserbyId = (userId: string): Promise<User | null> => firstValueFrom(this.getUserbyId$(userId));
|
||||
public getUserbyId$ = (userId: string): Observable<User | null> => {
|
||||
const cached = this.userByIdCache.get(userId);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const user$ = this.db.doc$<User>(`users/${userId}`).pipe(shareReplay({bufferSize: 1, refCount: true}));
|
||||
this.userByIdCache.set(userId, user$);
|
||||
return user$;
|
||||
};
|
||||
|
||||
public async login(user: string, password: string): Promise<string | null> {
|
||||
const aUser = await this.runInFirebaseContext(() => signInWithEmailAndPassword(this.auth, user, password));
|
||||
if (!aUser.user) return null;
|
||||
const dUser = await this.readUser(aUser.user.uid);
|
||||
if (!dUser) return null;
|
||||
await this.initSongUsage(dUser);
|
||||
this.iUser$.next(dUser);
|
||||
this.iUserId$.next(aUser.user.uid);
|
||||
|
||||
return aUser.user.uid;
|
||||
}
|
||||
|
||||
public loggedIn$: () => Observable<boolean> = () => this.authState$().pipe(map(_ => !!_));
|
||||
|
||||
public list$: () => Observable<User[]> = (): Observable<User[]> => this.users$;
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
await this.runInFirebaseContext(() => signOut(this.auth));
|
||||
this.iUser$.next(null);
|
||||
this.iUserId$.next(null);
|
||||
}
|
||||
|
||||
public async update$(uid: string, data: Partial<User>): Promise<void> {
|
||||
await this.db.doc<User>('users/' + uid).update(data);
|
||||
}
|
||||
|
||||
public async changePassword(user: string): Promise<void> {
|
||||
const url = environment.url;
|
||||
await this.runInFirebaseContext(() => sendPasswordResetEmail(this.auth, user, {url}));
|
||||
}
|
||||
|
||||
public async createNewUser(user: string, name: string, password: string): Promise<void> {
|
||||
const aUser = await this.runInFirebaseContext(() => createUserWithEmailAndPassword(this.auth, user, password));
|
||||
if (!aUser.user) return;
|
||||
const userId = aUser.user.uid;
|
||||
await this.db.doc('users/' + userId).set({name, chordMode: 'onlyFirst', songUsage: {}});
|
||||
const dUser = await this.readUser(aUser.user.uid);
|
||||
if (!dUser) return;
|
||||
this.iUser$.next(dUser);
|
||||
await this.router.navigateByUrl('/brand/new-user');
|
||||
}
|
||||
|
||||
public incSongCount = (songId: string) => this.updateSongUsage(songId, 1);
|
||||
public decSongCount = (songId: string) => this.updateSongUsage(songId, -1);
|
||||
|
||||
public async rebuildSongUsage(): Promise<SongUsageMigrationResult> {
|
||||
const currentUser = await firstValueFrom(this.iUser$.pipe(take(1)));
|
||||
if (!currentUser || !this.hasAdminRole(currentUser.role)) {
|
||||
throw new Error('Admin role required to rebuild songUsage.');
|
||||
}
|
||||
|
||||
const [users, shows] = await Promise.all([firstValueFrom(this.users$), firstValueFrom(this.showDataService.listRaw$())]);
|
||||
const songUsageByUserId: Record<string, Record<string, number>> = {};
|
||||
|
||||
users.forEach(user => {
|
||||
songUsageByUserId[user.id] = {};
|
||||
});
|
||||
|
||||
let showSongsProcessed = 0;
|
||||
for (const show of shows) {
|
||||
const ownerId = show.owner;
|
||||
if (!ownerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const showSongs = await firstValueFrom(this.showSongDataService.list$(show.id));
|
||||
const usage = songUsageByUserId[ownerId] ?? {};
|
||||
songUsageByUserId[ownerId] = usage;
|
||||
|
||||
for (const showSong of showSongs) {
|
||||
showSongsProcessed += 1;
|
||||
usage[showSong.songId] = (usage[showSong.songId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
users.map(user =>
|
||||
this.update$(user.id, {
|
||||
songUsage: songUsageByUserId[user.id] ?? {},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
usersProcessed: users.length,
|
||||
showsProcessed: shows.length,
|
||||
showSongsProcessed,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateSongUsage(songId: string, direction: number) {
|
||||
const user = await firstValueFrom(this.user$);
|
||||
if (!user) return null;
|
||||
|
||||
await this.db.doc<User>('users/' + user.id).update({
|
||||
[`songUsage.${songId}`]: increment(direction),
|
||||
});
|
||||
}
|
||||
|
||||
private async initSongUsage(user: User) {
|
||||
if (user.songUsage) return;
|
||||
await this.update$(user.id, {songUsage: {}});
|
||||
}
|
||||
|
||||
private hasAdminRole(role: string | null | undefined): boolean {
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return role.split(';').includes('admin');
|
||||
}
|
||||
|
||||
private authState$ = () => runInInjectionContext(this.environmentInjector, () => authState(this.auth));
|
||||
private runInFirebaseContext = <T>(factory: () => T): T => runInInjectionContext(this.environmentInjector, factory);
|
||||
private readUser$ = (uid: string) => this.db.doc$<User>('users/' + uid);
|
||||
private readUser: (uid: string) => Promise<User | null> = (uid: string) => firstValueFrom(this.readUser$(uid));
|
||||
public currentUser = (): Promise<User | null> => this.session.currentUser();
|
||||
public getUserbyId = (userId: string): Promise<User | null> => this.session.getUserbyId(userId);
|
||||
public getUserbyId$ = (userId: string): Observable<User | null> => this.session.getUserbyId$(userId);
|
||||
public login = (user: string, password: string): Promise<string | null> => this.session.login(user, password);
|
||||
public loggedIn$ = (): Observable<boolean> => this.session.loggedIn$();
|
||||
public list$ = (): Observable<User[]> => this.session.list$();
|
||||
public logout = (): Promise<void> => this.session.logout();
|
||||
public update$ = (uid: string, data: Partial<User>): Promise<void> => this.session.update$(uid, data);
|
||||
public changePassword = (user: string): Promise<void> => this.session.changePassword(user);
|
||||
public createNewUser = (user: string, name: string, password: string): Promise<void> => this.session.createNewUser(user, name, password);
|
||||
public incSongCount = (songId: string): Promise<void | null> => this.songUsage.incSongCount(songId);
|
||||
public decSongCount = (songId: string): Promise<void | null> => this.songUsage.decSongCount(songId);
|
||||
public rebuildSongUsage = (): Promise<SongUsageMigrationResult> => this.songUsage.rebuildSongUsage();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user