import {getDatabase, createOptions} from "@vostro/c2-utils/lib/database"; import {validateFindOptions, validateMutation} from "@vostro/c2-utils/lib/auth"; import {getCache} from "@vostro/c2-utils/lib/redis-cache"; import logger from "@vostro/c2-utils/lib/logger"; import Sequelize, {Op} from "sequelize"; import {v4 as uuid} from "uuid"; import bcrypt from "bcrypt"; import moment from "moment"; import {GraphQLError} from "graphql/error"; import { GraphQLString, GraphQLBoolean, GraphQLInputObjectType, GraphQLObjectType, GraphQLList, } from "graphql"; import { C2Utils } from "@vostro/c2-utils/lib/types"; import { WhereOptions } from 'sequelize'; const log = logger("models:user:"); async function testUsername(userName: string, count = 0, modelId: string, options: C2Utils.DataContext): Promise { // return userName; const db = await getDatabase(); const {User} = db.models; let u: string = userName; if (count > 0) { u += `${count}`; } let where: WhereOptions = { userName: u, }; if (modelId) { where.id = { [Op.ne]: modelId, }; } const usernameCount = await User.count(createOptions(options, { where, override: true, paranoid: false, })); if (usernameCount === 0) { return u; } count = count + 1; return testUsername(userName, count, modelId, options); } const GQLRegisterInput = new GraphQLInputObjectType({ name: "GQLUserRegister", fields: { userName: { type: GraphQLString, }, firstName: { type: GraphQLString, }, lastName: { type: GraphQLString, }, email: { type: GraphQLString, }, phone: { type: GraphQLString, }, password: { type: GraphQLString, }, captcha: { type: GraphQLString, }, }, }); const GQLPermission = new GraphQLObjectType({ name: "GQLPermission", fields: { level: { type: GraphQLString, }, type: { type: GraphQLString, }, name: { type: GraphQLString, }, }, }); const GQLPermissions = new GraphQLList(GQLPermission); const UserModel = { name: "User", define: { userName: { type: Sequelize.STRING, allowNull: false, unique: true, }, disabled: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false, }, email: { type: Sequelize.TEXT, allowNull: true, }, firstName: { type: Sequelize.STRING, allowNull: true, }, lastName: { type: Sequelize.STRING, allowNull: true, }, }, relationships: [{ type: "belongsTo", model: "Role", name: "role", options: { foreignKey: "roleId", }, }, { type: "hasMany", model: "UserAuth", name: "auths", options: { as: "auths", foreignKey: "userId", }, }], expose: { instanceMethods: { query: { permissions: { type: GQLPermissions, }, }, }, classMethods: { query: { getCurrentUser: { type: "User", args: {}, }, getPermissions: { type: GQLPermissions, args: {}, }, isLoggedIn: { type: GraphQLBoolean, args: {}, }, }, mutations: { login: { type: GraphQLBoolean, args: { userName: { type: GraphQLString, }, password: { type: GraphQLString, }, captcha: { type: GraphQLString, }, }, }, register: { type: "User", args: { input: { type: GQLRegisterInput, }, }, }, logout: { type: GraphQLBoolean, }, getToken: { type: GraphQLString, args: { userName: { type: GraphQLString, }, password: { type: GraphQLString, }, }, }, }, }, }, options: { tableName: "users", indexes: [{ unique: true, fields: ["userName"], }, { fields: ["email"], }, { fields: ["firstName", "lastName", "email", "userName"], }], hooks: { beforeValidate: [async function beforeValidate(model: { id?: any; userName: any; }, options = {}) { const { userName, } = model; let u = await testUsername(userName, 0, model.id, options); if (u !== userName) { model.userName = u; } return model; }], beforeFind: [async function beforeFind(options: C2Utils.FindOptions) { return validateFindOptions("User", options, Op.and, (user, roleLevel) => { switch (roleLevel) { case "self": return { "id": user.id, }; } return {}; }); }], beforeCreate: [async function beforeCreate(instance: any, options: C2Utils.FindOptions) { return validateMutation("User", "create", options, instance, async(user, model, roleLevel, context) => { const db = await getDatabase(); const {Role} = db.models; const userRole = await Role.findOne({ where: { name: "user", }, override: true, }); return model.roleId === userRole.id; }, true); }], beforeUpdate: [async function beforeUpdate(instance: any, options: C2Utils.FindOptions) { return validateMutation("User", "update", options, instance, async(user, model, roleLevel, context) => { const db = await getDatabase(); const {Role} = db.models; const userRole = await Role.findOne({ where: { name: "user", }, override: true, }); return model.roleId === userRole.id; }, true); }], beforeDestroy: [async function beforeDestroy(instance: any, options: C2Utils.FindOptions) { return validateMutation("User", "destroy", options, instance, undefined, true); }], }, classMethods: { async getToken(args: { userName: any; password: any; }, context: any) { const {userName, password} = args; //TODO: check for access? const db = await getDatabase(); const {UserAuth} = db.models; const user = await UserModel.options.classMethods.loginWithUser({ userName, password, doNotLogin: true, }, context); const authToken = await UserAuth.findOrCreate({ override: true, context, where: { userId: user.id, type: "token", }, defaults: { userId: user.id, type: "token", token: uuid(), }, }); if (authToken.length === 0) { throw new GraphQLError("Invalid username/password"); } return authToken[0].token; }, getCurrentUser(args: any, context: { getUser: () => any; }) { return context.getUser(); }, async isLoggedIn(args: any, context: { getUser: () => any; }) { const user = await context.getUser(); return !(!user); }, async login(args: { userName: any; password: any; captcha: any; doNotLogin?: boolean | undefined; }, context: any) { await UserModel.options.classMethods.loginWithUser(args, context); return true; }, async loginWithUser({ userName, password, doNotLogin = false, }: { userName: string; password: string; doNotLogin?: boolean; }, context: any) { try { const db = await getDatabase(); const { User, UserAuth, } = db.models; const user = await User.findOne(createOptions(context, { where: { userName: userName, }, override: true, include: [{ where: { type: "local", }, model: UserAuth, as: "auths", }], })); if (user) { // compare passwords // password hash is saved as token in userAuths // const hash = await bcrypt.hash(password, user.id); const success = await bcrypt.compare(password, user.auths[0].token); if (success) { // const {req} = context.getRequest(); // if (captcha) { // if (req.session.captcha === captcha) { // if (!doNotLogin) { // await context.login(user); // } // return user; // } // throw new GraphQLError("Invalid captcha provided"); // } else { if (!doNotLogin) { await context.login(user); } // } return user; } throw new GraphQLError("Invalid username/password"); } return false; } catch (err: any) { log.error(err); throw err; } }, async register({ input, }: any, context: { getRequest: () => any; override: any; login: (arg0: any) => any; }) { log.debug("register", input); try { const req = context.getRequest(); if (input.captcha) { if (req.session.captcha !== input.captcha) { throw new GraphQLError("Invalid captcha provided"); } } const db = await getDatabase(); const {User, UserAuth, Role} = db.models; let password = (input.password || moment().format("ddddhmmss")); const typeName = context.override ? input.type : "user"; const role = await Role.findOne(createOptions(context, { where: { name: input.roleName ? input.roleName : typeName, }, })); input.roleId = role.id; let user = await User.create(Object.assign({}, input), { override: true, }); if (user) { await UserAuth.create({ type: "local", token: password, userId: user.id, }, createOptions(context, {})); user = await User.findOne(createOptions(context, { where: { id: user.id, }, override: true, include: [{ where: { type: "local", }, model: UserAuth, as: "auths", }], })); await context.login(user); return user; } } catch (err: any) { log.error(err); throw err; } return null; }, async logout(args: any, context: C2Utils.DataContext) { try { if(context.logout) { return context.logout(); } } catch (err: any) { log.error(err); } return false; }, async getPermissions(args: any, context: C2Utils.DataContext) { const {role} = context; return getPermissions(role, context); }, }, instanceMethods: { // async role(args, context) { // return this.getRole({context}); // }, async permission(this:any, args: any, context: C2Utils.DataContext) { const role = await this.getRole({context}); return getPermissions(role, context); }, }, }, }; async function getPermissions(role: { id: any; }, context: C2Utils.DataContext | undefined) { const db = await getDatabase(); const {RolePermission, Permission} = db.models; const cache = getCache(); return cache.getGeneric(`permissions-${role.id}`, async() => { const permissions = await RolePermission.findAll(createOptions(context, { where: { roleId: role.id, }, include: [ { model: Permission, as: "permission", } ], override: true, })); return permissions.map((p: { level: any; permission: { name: any; type: any; }; }) => { return { level: p.level, name: p.permission.name, type: p.permission.type, }; }); }, context); } export default UserModel;