import { Collection , Db , ObjectID , IndexOptions } from 'mongodb' ; import { CreateUserServicePassword , DatabaseInterfaceServicePassword , User } from '@accounts/types' ; import { toMongoID } from './utils' ; export interface MongoUser { _id ? : string | object ; username ? : string ; services : { password ? : { bcrypt : string ; } ; } ; emails ? : [ { address : string ; verified : boolean ; } ] ; [ key ] : any ; } export interface MongoServicePasswordOptions { /**
     * Mongo database object.
     */ database : Db ; /**
     * The users collection name.
     * Default 'users'.
     */ userCollectionName ? : string ; /**
     * The timestamps for the users collection.
     * Default 'createdAt' and 'updatedAt'.
     */ timestamps ? : { createdAt : string ; updatedAt : string ; } ; /**
     * Should the user collection use _id as string or ObjectId.
     * Default 'true'.
     */ convertUserIdToMongoObjectId ? : boolean ; /**
     * Perform case intensitive query for user name.
     * Default 'true'.
     */ caseSensitiveUserName ? : boolean ; /**
     * Function that generate the id for new objects.
     */ idProvider ? : ( ) => string | object ; /**
     * Function that generate the date for the timestamps.
     * Default to `(date?: Date) => (date ? date.getTime() : Date.now())`.
     */ dateProvider ? : ( date ? : Date ) => any ; } const defaultOptions = { userCollectionName : 'users' , timestamps : { createdAt : 'createdAt' , updatedAt : 'updatedAt' , } , convertUserIdToMongoObjectId : true , caseSensitiveUserName : true , dateProvider : ( date ? ) => ( date ? date . getTime ( ) : Date . now ( ) ) , } ; export class MongoServicePassword implements DatabaseInterfaceServicePassword { private options : MongoServicePasswordOptions & typeof defaultOptions ; private database : Db ; private userCollection : Collection ; constructor ( options ) { this . options = { ... defaultOptions , ... options , timestamps : { ... defaultOptions . timestamps , ... options . timestamps } , } ; this . database = this . options . database ; this . userCollection = this . database . collection ( this . options . userCollectionName ) ; } param options public async setupIndexes ( options = { } ) { await this . userCollection . createIndex ( 'username' , { ... options , unique : true , sparse : true , } ) ; await this . userCollection . createIndex ( 'emails.address' , { ... options , unique : true , sparse : true , } ) ; await this . userCollection . createIndex ( 'services.email.verificationTokens.token' , { ... options , sparse : true , } ) ; await this . userCollection . createIndex ( 'services.password.reset.token' , { ... options , sparse : true , } ) ; } /**
     * Create a new user by providing an email and/or a username and password.
     * Emails are saved lowercased.
     */ public async createUser ( { password , username , email , ... cleanUser } ) { const user = { ... cleanUser , services : { password : { bcrypt : password , } , } , [ this . options . timestamps . createdAt ] : this . options . dateProvider ( ) , [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } ; if ( username ) { user . username = username ; } if ( email ) { user . emails = [ { address : email . toLowerCase ( ) , verified : false } ] ; } if ( this . options . idProvider ) { user . _id = this . options . idProvider ( ) ; } const ret = await this . userCollection . insertOne ( user ) ; return ( ( ret . insertedId ?? ret . ops [ 0 ] . _id ) as ObjectID ) . toString ( ) ; } param userId public async findUserById ( userId ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const user = await this . userCollection . findOne ( { _id : id } ) ; if ( user ) { user . id = user . _id . toString ( ) ; } return user ; } param email public async findUserByEmail ( email ) { const user = await this . userCollection . findOne ( { 'emails.address' : email . toLowerCase ( ) , } ) ; if ( user ) { user . id = user . _id . toString ( ) ; } return user ; } param email public async findUserByUsername ( username ) { const filter = this . options . caseSensitiveUserName ? { username } : { $where : `obj.username && (obj.username.toLowerCase() === "${ username . toLowerCase ( ) }")` , } ; const user = await this . userCollection . findOne ( filter ) ; if ( user ) { user . id = user . _id . toString ( ) ; } return user ; } param userId public async findPasswordHash ( userId ) { const user = await this . findUserById ( userId ) ; return user ?. services ?. password ?. bcrypt ?? null ; } param token public async findUserByEmailVerificationToken ( token ) { const user = await this . userCollection . findOne ( { 'services.email.verificationTokens.token' : token , } ) ; if ( user ) { user . id = user . _id . toString ( ) ; } return user ; } param token public async findUserByResetPasswordToken ( token ) { const user = await this . userCollection . findOne ( { 'services.password.reset.token' : token , } ) ; if ( user ) { user . id = user . _id . toString ( ) ; } return user ; } param userId param newEmail param verified public async addEmail ( userId , newEmail , verified ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const ret = await this . userCollection . updateOne ( { _id : id } , { $addToSet : { emails : { address : newEmail . toLowerCase ( ) , verified , } , } , $set : { [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } , } ) ; if ( ( ret . modifiedCount && ret . modifiedCount === 0 ) || ( ret . result && ret . result . nModified === 0 ) ) { throw new Error ( 'User not found' ) ; } } param userId param email public async removeEmail ( userId , email ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const ret = await this . userCollection . updateOne ( { _id : id } , { $pull : { emails : { address : email . toLowerCase ( ) } } , $set : { [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } , } ) ; if ( ( ret . modifiedCount && ret . modifiedCount === 0 ) || ( ret . result && ret . result . nModified === 0 ) ) { throw new Error ( 'User not found' ) ; } } param userId param email public async verifyEmail ( userId , email ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const ret = await this . userCollection . updateOne ( { _id : id , 'emails.address' : email } , { $set : { 'emails.$.verified' : true , [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } , $pull : { 'services.email.verificationTokens' : { address : email } } , } ) ; if ( ret . result . nModified === 0 ) { throw new Error ( 'User not found' ) ; } } param userId param newUsername public async setUsername ( userId , newUsername ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const ret = await this . userCollection . updateOne ( { _id : id } , { $set : { username : newUsername , [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } , } ) ; if ( ret . result . nModified === 0 ) { throw new Error ( 'User not found' ) ; } } param userId param newPassword public async setPassword ( userId , newPassword ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; const ret = await this . userCollection . updateOne ( { _id : id } , { $set : { 'services.password.bcrypt' : newPassword , [ this . options . timestamps . updatedAt ] : this . options . dateProvider ( ) , } , $unset : { 'services.password.reset' : '' , } , } ) ; if ( ret . result . nModified === 0 ) { throw new Error ( 'User not found' ) ; } } param userId param email param token public async addEmailVerificationToken ( userId , email , token ) { const _id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; await this . userCollection . updateOne ( { _id } , { $push : { 'services.email.verificationTokens' : { token , address : email . toLowerCase ( ) , when : this . options . dateProvider ( ) , } , } , } ) ; } param userId param email param token param reason public async addResetPasswordToken ( userId , email , token , reason ) { const _id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; await this . userCollection . updateOne ( { _id } , { $push : { 'services.password.reset' : { token , address : email . toLowerCase ( ) , when : this . options . dateProvider ( ) , reason , } , } , } ) ; } param userId public async removeAllResetPasswordTokens ( userId ) { const id = this . options . convertUserIdToMongoObjectId ? toMongoID ( userId ) : userId ; await this . userCollection . updateOne ( { _id : id } , { $unset : { 'services.password.reset' : '' , } , } ) ; } }