import { injectable , inject } from 'inversify' ; import { MONGO_EVENT_STORE_FACTORY , MongoEventStoreFactory , MONGO_SNAPSHOT_STORE_FACTORY , MongoSnapshotStoreFactory , } from '@centsideas/event-sourcing' ; import { Email , Timestamp , UserId , SessionId , AccessToken , EmailSignInToken , } from '@centsideas/types' ; import { EventTopics , TokenExpirationTimes , AuthenticationEventNames } from '@centsideas/enums' ; import { PersistedEvent } from '@centsideas/models' ; import { SecretsConfig } from '@centsideas/config' ; import { serializeEvent } from '@centsideas/rpc' ; import { Session } from './session' ; import { RefreshToken } from './refresh-token' ; import { UserReadAdapter } from './user-read.adapter' ; import { AuthenticationConfig } from './authentication.config' ; import { GoogleApiAdapter } from './google-api.adapter' ; import * as Errors from './session.errors' ; @ injectable ( ) export class AuthenticationService { private readonly refreshTokenSecret = this . secretesConfig . get ( 'secrets.tokens.refresh' ) ; private readonly accessTokenSecret = this . secretesConfig . get ( 'secrets.tokens.access' ) ; private readonly signInTokenSecret = this . secretesConfig . get ( 'secrets.tokens.signin' ) ; private readonly databaseUrl = this . config . get ( 'authentication.database.url' ) ; private readonly databaseName = this . config . get ( 'authentication.database.name' ) ; private readonly snapshotDistance = 50 ; private eventStore = this . eventStoreFactory ( { url : this . databaseUrl , name : this . databaseName , topic : EventTopics . Session , } ) ; private snapshotStore = this . snapshotStoreFactory ( { url : this . databaseUrl , name : this . databaseName , } ) ; constructor ( private secretesConfig , private config , private userReadAdapter , private googleApiAdapter , @ inject ( MONGO_EVENT_STORE_FACTORY ) private eventStoreFactory , @ inject ( MONGO_SNAPSHOT_STORE_FACTORY ) private snapshotStoreFactory ) { } async requestEmailSignIn ( emailString ) { const sessionId = SessionId . generate ( ) ; const email = Email . fromString ( emailString ) ; const session = Session . requestEmailSignIn ( sessionId , email , Timestamp . now ( ) ) ; await this . store ( session ) ; } async confirmEmailSignIn ( signInToken ) { const { sessionId , email } = EmailSignInToken . fromString ( signInToken , this . signInTokenSecret ) ; const session = await this . build ( sessionId ) ; const existingUser = await this . userReadAdapter . getUserByEmail ( email ) ; const userId = existingUser ? UserId . fromString ( existingUser . id ) : UserId . generate ( ) ; session . confirmEmailSignIn ( userId , ! existingUser , email , Timestamp . now ( ) ) ; await this . store ( session ) ; const accessToken = new AccessToken ( sessionId , userId ) ; const refreshToken = new RefreshToken ( sessionId , userId ) ; return { accessToken : accessToken . sign ( this . accessTokenSecret , TokenExpirationTimes . Access ) , refreshToken : refreshToken . sign ( this . refreshTokenSecret , TokenExpirationTimes . Refresh ) , userId : userId . toString ( ) , } ; } googleSignInUrl ( ) { return { url : this . googleApiAdapter . getSignInUrl } ; } async googleSignIn ( code ) { const requestedAt = Timestamp . now ( ) ; const googleAccessToken = await this . googleApiAdapter . getAccessToken ( code ) ; const { email } = await this . googleApiAdapter . getUserInfo ( googleAccessToken ) ; const existingUser = await this . userReadAdapter . getUserByEmail ( email ) ; const userId = existingUser ? UserId . fromString ( existingUser . id ) : UserId . generate ( ) ; const sessionId = SessionId . generate ( ) ; const session = Session . googleSignIn ( sessionId , userId , email , ! existingUser , requestedAt , Timestamp . now ( ) ) ; await this . store ( session ) ; const accessToken = new AccessToken ( sessionId , userId ) ; const refreshToken = new RefreshToken ( sessionId , userId ) ; return { accessToken : accessToken . sign ( this . accessTokenSecret , TokenExpirationTimes . Access ) , refreshToken : refreshToken . sign ( this . refreshTokenSecret , TokenExpirationTimes . Refresh ) , userId : userId . toString ( ) , } ; } async refresTokens ( currentRefreshToken ) { const { sessionId , userId } = RefreshToken . fromString ( currentRefreshToken , this . refreshTokenSecret ) ; const session = await this . build ( sessionId ) ; session . refreshTokens ( ) ; await this . store ( session ) ; const accessToken = new AccessToken ( sessionId , userId ) ; const refreshToken = new RefreshToken ( sessionId , userId ) ; return { accessToken : accessToken . sign ( this . accessTokenSecret , TokenExpirationTimes . Access ) , refreshToken : refreshToken . sign ( this . refreshTokenSecret , TokenExpirationTimes . Refresh ) , userId : userId . toString ( ) , } ; } async signOut ( refreshToken ) { const { sessionId } = RefreshToken . fromString ( refreshToken , this . refreshTokenSecret ) ; const session = await this . build ( sessionId ) ; session . signOut ( Timestamp . now ( ) ) ; await this . store ( session ) ; } async revokeRefreshToken ( sessionId ) { const session = await this . build ( SessionId . fromString ( sessionId ) ) ; session . revokeRefreshToken ( ) ; await this . store ( session ) ; } async getEvents ( after ? ) { const events = await this . eventStore . getEvents ( after || - 1 ) ; return events . map ( serializeEvent ) ; } async getEventsByUserId ( user ) { const dbCollection = await this . eventStore . _getCollection ( ) ; const result = await dbCollection . aggregate ( [ { $match : { $or : [ { name : AuthenticationEventNames . GoogleSignInConfirmed , 'data.userId' : user . toString ( ) } , { name : AuthenticationEventNames . SignInConfirmed , 'data.userId' : user . toString ( ) } , ] , } , } , { $group : { _id : '$streamId' } , } , ] ) ; const streamIds = ( await result . toArray ( ) ) . map ( ( s ) => s . _id ) ; const events = dbCollection . find ( { streamId : { $in : streamIds } } ) ; return ( await events . toArray ( ) ) . map ( serializeEvent ) ; } private async build ( id ) { const snapshot = await this . snapshotStore . get ( id ) ; const events = snapshot ? await this . eventStore . getStream ( id . toString ( ) , snapshot . version ) : await this . eventStore . getStream ( id . toString ( ) ) ; if ( ! events ?. length ) throw new Errors . SessionNotFound ( id ) ; return Session . buildFrom ( events , snapshot ) ; } private async store ( session ) { await this . eventStore . store ( session . flushEvents ( ) , session . persistedAggregateVersion ) ; const snapshot = await this . snapshotStore . get ( session . aggregateId ) ; if ( session . aggregateVersion - ( snapshot ?. version || 0 ) > this . snapshotDistance ) await this . snapshotStore . store ( session . snapshot ) ; } }