Created
November 8, 2017 13:28
-
-
Save nodkz/812519ca9473b28493122872ae57e9c3 to your computer and use it in GitHub Desktop.
Mongoose with flow example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
/* eslint-disable func-names */ | |
import { Schema } from 'mongoose'; | |
import DB from 'schema/db'; | |
import composeWithMongoose from 'graphql-compose-mongoose'; | |
import composeWithRelay from 'graphql-compose-relay'; | |
import crypto from 'crypto'; | |
import bcrypt from 'bcrypt'; | |
import type { $Request } from 'express'; | |
import { AvatarUrlSchema, type AvatarUrlDoc } from 'schema/customTypes/avatarUrl'; | |
import { Cabinet, type CabinetDoc } from 'schema/cabinet'; | |
export const UserSchema = new Schema( | |
{ | |
email: { | |
type: String, | |
set: v => v.toLowerCase().trim(), | |
// validate: (v) => UserSchema.isValidEmail(v), TODO | |
required: true, | |
}, | |
provider: { | |
type: String, | |
required: true, | |
}, | |
providerId: { | |
type: String, | |
set: v => v.toLowerCase().trim(), | |
description: 'String code of provider', | |
required: true, | |
}, | |
token: { | |
type: String, | |
required: true, | |
default() { | |
// if token not exists, generate random password | |
return this.encryptPassword(Math.random().toString()); | |
}, | |
}, | |
name: String, | |
avatarUrl: AvatarUrlSchema, | |
oneTimeTokenExp: String, | |
meta: { | |
type: Schema.Types.Mixed, | |
}, | |
lastIp: String, | |
lastLoginAt: Date, | |
}, | |
{ | |
setDefaultsOnInsert: true, | |
timestamps: true, | |
collection: 'user', | |
} | |
); | |
UserSchema.index({ email: 1 }, { background: true }); | |
UserSchema.index({ provider: 1, providerId: 1 }, { unique: true, background: true }); | |
export class UserDoc /* :: extends Mongoose$Document */ { | |
email: string; | |
provider: string; | |
providerId: string; | |
token: string; | |
name: ?string; | |
avatarUrl: ?(AvatarUrlDoc | $Shape<AvatarUrlDoc>); | |
oneTimeTokenExp: ?string; | |
meta: ?any; | |
lastIp: ?string; | |
lastLoginAt: ?Date; | |
_plainPassword: ?string; // internal field, exists only on object create | |
_oneTimeToken: ?string; // internal field | |
/** @deprecated remove when will be unused in other places of codebase */ | |
static async findByProviderDeprecated(provider, providerId, cb) { | |
return this.findOne({ provider, providerId: providerId.toLowerCase().trim() }, cb); | |
} | |
static isValidEmail(email) { | |
const r = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; // eslint-disable-line | |
return r.test(email); | |
} | |
static async findByProvider(provider, providerId): Promise<?UserDoc> { | |
return this.findOne({ | |
provider, | |
providerId: providerId.toLowerCase().trim(), | |
}).exec(); | |
} | |
static async findByEmail(email): Promise<?UserDoc> { | |
return this.findOne({ email }); | |
} | |
static async findByEmailOrCreate({ email, password, provider, providerId }): Promise<UserDoc> { | |
const existsUser = await this.findOne({ email }); | |
if (existsUser) return existsUser; | |
const newUser = new this({ email, password, provider, providerId }); | |
await newUser.save(); | |
return newUser; | |
} | |
set password(password: string) { | |
this._plainPassword = password; | |
this.token = this.encryptPassword(password); | |
} | |
get password(): ?string { | |
return this._plainPassword; | |
} | |
encryptPassword(password: string): string { | |
return bcrypt.hashSync(password, 10); | |
} | |
checkPassword(password: string): boolean { | |
if (this.token) { | |
return bcrypt.compareSync(password, this.token); | |
} | |
return false; | |
} | |
genPassword(len: number = 8): string { | |
const newPass = crypto | |
.randomBytes(Math.ceil(len * 3 / 4)) // eslint-disable-line | |
.toString('base64') // convert to base64 format | |
.slice(0, len) // return required number of characters | |
.replace(/\+/g, '0') // replace '+' with '0' | |
.replace(/\//g, '0'); // replace '/' with '0' | |
this.password = newPass; | |
return newPass; | |
} | |
// by default valid 1 hour | |
oneTimeTokenGenerate(EXPIRED_AFTER?: number = 3600000) { | |
let token; | |
// If current one-time-token is valid at least half of expire period, then return existed | |
// The reason is that user may require several emails in short period of time | |
// So we should return same token, cause user may click by link from any email. | |
if (this.oneTimeTokenExp) { | |
const [tokenInDB, expireAtInDB] = this.oneTimeTokenExp.split(':'); | |
const expireAtInDBInt = parseInt(expireAtInDB, 10); | |
if (expireAtInDBInt > Date.now() + EXPIRED_AFTER / 2) { | |
token = tokenInDB; | |
} | |
} | |
// Generate new token | |
if (!token) { | |
token = crypto.randomBytes(20).toString('hex'); | |
const expireAt = Date.now() + EXPIRED_AFTER; | |
this.oneTimeTokenExp = `${token}:${expireAt}`; | |
} | |
this._oneTimeToken = token; | |
return token; | |
} | |
oneTimeTokenCheck(token: string): boolean { | |
if (!token || !this.oneTimeTokenExp) return false; | |
const [tokenInDB, expireAt] = this.oneTimeTokenExp.split(':'); | |
return parseInt(expireAt, 10) > Date.now() && token === tokenInDB; | |
} | |
oneTimeTokenRevoke(): void { | |
this.oneTimeTokenExp = null; | |
} | |
async oneTimeLoginGenerate(EXPIRED_AFTER: number): Promise<string> { | |
this.oneTimeTokenRevoke(); | |
const token = this.oneTimeTokenGenerate(EXPIRED_AFTER); | |
await this.save(); | |
const email64 = Buffer.from(this.email).toString('base64'); | |
return `${email64}:${token}`; | |
} | |
touchLastLoginAt(req: $Request): this { | |
let ip = ''; | |
if (req) { | |
ip = | |
(req.headers && req.headers['x-forwarded-for']) || | |
(req.connection && req.connection.remoteAddress); | |
} | |
this.lastIp = ip; | |
this.lastLoginAt = new Date(); | |
return this; | |
} | |
async getCabinets(all: boolean = false): Promise<Array<CabinetDoc>> { | |
if (!this.email) return Promise.resolve([]); | |
if (all) { | |
return Cabinet.find({ | |
$or: [{ users: this.email }, { owner: this.email }], | |
}).exec(); | |
} | |
return Cabinet.find({ owner: this.email }).exec(); | |
} | |
} | |
UserSchema.loadClass(UserDoc); | |
export const User = DB.data.model('User', UserSchema); | |
export const UserTC = composeWithRelay(composeWithMongoose(User)); | |
UserTC.getResolver('createOne') | |
.getArgTC('input') | |
.getFieldTC('record') | |
.addFields({ | |
password: 'String!', | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What's the definition of DB? With flow 1.7.0, the type of
User
seems to be justUserDoc
and has no mongoose methods.