diff --git a/source/app.d b/source/app.d index d70059a..36d00e4 100644 --- a/source/app.d +++ b/source/app.d @@ -8,11 +8,13 @@ import vibe.d; import userman.web; +import userman.mongocontroller; shared static this() { auto usettings = new UserManSettings; - auto uctrl = new UserManController(usettings); + auto uctrl = new MongoController(usettings); + auto uweb = new UserManWebInterface(uctrl); auto router = new URLRouter; router.registerUserManWebInterface(uctrl); diff --git a/source/userman/controller.d b/source/userman/controller.d index 3f8cf93..805b2ff 100644 --- a/source/userman/controller.d +++ b/source/userman/controller.d @@ -26,31 +26,18 @@ import std.string; class UserManController { - private { - MongoCollection m_users; - MongoCollection m_groups; + protected { UserManSettings m_settings; } this(UserManSettings settings) { m_settings = settings; - - auto db = connectMongoDB("127.0.0.1").getDatabase(m_settings.databaseName); - m_users = db["userman.users"]; - m_groups = db["userman.groups"]; - - m_users.ensureIndex(["name": 1], IndexFlags.Unique); - m_users.ensureIndex(["email": 1], IndexFlags.Unique); } @property UserManSettings settings() { return m_settings; } - bool isEmailRegistered(string email) - { - auto bu = m_users.findOne(["email": email], ["auth": true]); - return !bu.isNull() && bu.auth.method.get!string.length > 0; - } + abstract bool isEmailRegistered(string email); void validateUser(User usr) { @@ -58,16 +45,9 @@ class UserManController { validateEmail(usr.email); } - void addUser(User usr) - { - validateUser(usr); - enforce(m_users.findOne(["name": usr.name]).isNull(), "The user name is already taken."); - enforce(m_users.findOne(["email": usr.email]).isNull(), "The email address is already in use."); - usr._id = BsonObjectID.generate(); - m_users.insert(usr); - } + abstract void addUser(User usr); - BsonObjectID registerUser(string email, string name, string full_name, string password) + string registerUser(string email, string name, string full_name, string password) { email = email.toLower(); name = name.toLower(); @@ -77,7 +57,6 @@ class UserManController { auto need_activation = m_settings.requireAccountValidation; auto user = new User; - user._id = BsonObjectID.generate(); user.active = !need_activation; user.name = name; user.fullName = full_name; @@ -86,76 +65,73 @@ class UserManController { user.email = email; if( need_activation ) user.activationCode = generateActivationCode(); + addUser(user); if( need_activation ) resendActivation(email); - return user._id; + return user.id; } - BsonObjectID inviteUser(string email, string full_name, string message) + string inviteUser(string email, string full_name, string message) { email = email.toLower(); validateEmail(email); - auto existing = m_users.findOne(["email": email], ["_id": true]); - if( !existing.isNull() ) return existing._id.get!BsonObjectID; - - auto user = new User; - user._id = BsonObjectID.generate(); - user.email = email; - user.name = email; - user.fullName = full_name; - addUser(user); - - if( m_settings.mailSettings ){ - auto msg = new MemoryOutputStream; - parseDietFileCompat!("userman.mail.invitation.dt", - User, "user", - string, "serviceName", - URL, "serviceUrl")(msg, - user, - m_settings.serviceName, - m_settings.serviceUrl); - - auto mail = new Mail; - mail.headers["From"] = m_settings.serviceName ~ " <" ~ m_settings.serviceEmail ~ ">"; - mail.headers["To"] = email; - mail.headers["Subject"] = "Invitation"; - mail.headers["Content-Type"] = "text/html; charset=UTF-8"; - mail.bodyText = cast(string)msg.data(); - - sendMail(m_settings.mailSettings, mail); + try { + return getUserByEmail(email).id; + } + catch (Exception e) { + auto user = new User; + user.email = email; + user.name = email; + user.fullName = full_name; + addUser(user); + + if( m_settings.mailSettings ){ + auto msg = new MemoryOutputStream; + parseDietFileCompat!("userman.mail.invitation.dt", + User, "user", + string, "serviceName", + URL, "serviceUrl")(msg, + user, + m_settings.serviceName, + m_settings.serviceUrl); + + auto mail = new Mail; + mail.headers["From"] = m_settings.serviceName ~ " <" ~ m_settings.serviceEmail ~ ">"; + mail.headers["To"] = email; + mail.headers["Subject"] = "Invitation"; + mail.headers["Content-Type"] = "text/html; charset=UTF-8"; + mail.bodyText = cast(string)msg.data(); + + sendMail(m_settings.mailSettings, mail); + } + + return user.id; } - - return user._id; } void activateUser(string email, string activation_code) { email = email.toLower(); - auto busr = m_users.findOne(["email": email]); - enforce(!busr.isNull(), "There is no user account for the specified email address."); - enforce(!busr.active, "This user account is already activated."); - enforce(busr.activationCode.get!string == activation_code, "The activation code provided is not valid."); - busr.active = true; - busr.activationCode = ""; - m_users.update(["_id": busr._id], busr); + auto user = getUserByEmail(email); + enforce(!user.active, "This user account is already activated."); + enforce(user.activationCode == activation_code, "The activation code provided is not valid."); + user.active = true; + user.activationCode = ""; + updateUser(user); } void resendActivation(string email) { email = email.toLower(); - auto busr = m_users.findOne(["email": email]); - enforce(!busr.isNull(), "There is no user account for the specified email address."); - enforce(!busr.active, "The user account is already active."); - - auto user = new User; - deserializeBson(user, busr); + auto user = getUserByEmail(email); + enforce(!user.active, "The user account is already active."); auto msg = new MemoryOutputStream; parseDietFileCompat!("userman.mail.activation.dt", @@ -181,8 +157,10 @@ class UserManController { auto usr = getUserByEmail(email); string reset_code = generateActivationCode(); - BsonDate expire_time = BsonDate(Clock.currTime() + dur!"hours"(24)); - m_users.update(["_id": usr._id], ["$set": ["resetCode": Bson(reset_code), "resetCodeExpireTime": Bson(expire_time)]]); + SysTime expire_time = Clock.currTime() + dur!"hours"(24); + usr.resetCode = reset_code; + usr.resetCodeExpireTime = expire_time; + updateUser(usr); if( m_settings.mailSettings ){ auto msg = new MemoryOutputStream; @@ -207,93 +185,36 @@ class UserManController { validatePassword(new_password, new_password); auto usr = getUserByEmail(email); enforce(usr.resetCode.length > 0, "No password reset request was made."); - enforce(Clock.currTime() < usr.resetCodeExpireTime.toSysTime(), "Reset code is expired, please request a new one."); - m_users.update(["_id": usr._id], ["$set": ["resetCode": ""]]); + enforce(Clock.currTime() < usr.resetCodeExpireTime, "Reset code is expired, please request a new one."); + usr.resetCode = ""; + updateUser(usr); auto code = usr.resetCode; enforce(reset_code == code, "Invalid request code, please request a new one."); - m_users.update(["_id": usr._id], ["$set": ["auth.passwordHash": generateSimplePasswordHash(new_password)]]); - } - - User getUser(BsonObjectID id) - { - auto busr = m_users.findOne(["_id": id]); - enforce(!busr.isNull(), "The specified user id is invalid."); - auto ret = new User; - deserializeBson(ret, busr); - return ret; + usr.auth.passwordHash = generateSimplePasswordHash(new_password); + updateUser(usr); } - User getUserByName(string name) - { - name = name.toLower(); - - auto busr = m_users.findOne(["name": name]); - enforce(!busr.isNull(), "The specified user name is not registered."); - auto ret = new User; - deserializeBson(ret, busr); - return ret; - } + abstract User getUser(string id); - User getUserByEmail(string email) - { - email = email.toLower(); + abstract User getUserByName(string name); - auto busr = m_users.findOne(["email": email]); - enforce(!busr.isNull(), "The specified email address is not registered."); - auto ret = new User; - deserializeBson(ret, busr); - return ret; - } + abstract User getUserByEmail(string email); - User getUserByEmailOrName(string email_or_name) - { - auto busr = m_users.findOne(["$or": [["email": email_or_name.toLower()], ["name": email_or_name]]]); - enforce(!busr.isNull(), "The specified email address or user name is not registered."); - auto ret = new User; - deserializeBson(ret, busr); - return ret; - } + abstract User getUserByEmailOrName(string email_or_name); - void enumerateUsers(int first_user, int max_count, void delegate(ref User usr) del) - { - foreach( busr; m_users.find(["query": null, "orderby": ["name": 1]], null, QueryFlags.None, first_user, max_count) ){ - if (max_count-- <= 0) break; - auto usr = deserializeBson!User(busr); - del(usr); - } - } + abstract void enumerateUsers(int first_user, int max_count, void delegate(ref User usr) del); - long getUserCount() - { - return m_users.count(Bson.emptyObject); - } + abstract long getUserCount(); - void deleteUser(BsonObjectID user_id) - { - m_users.remove(["_id": user_id]); - } + abstract void deleteUser(string user_id); - void updateUser(User user) - { - validateUser(user); - enforce(m_settings.useUserNames || user.name == user.email, "User name must equal email address if user names are not used."); + abstract void updateUser(User user); - m_users.update(["_id": user._id], user); - } - - void addGroup(string name, string description) - { - enforce(m_groups.findOne(["name": name]).isNull(), "A group with this name already exists."); - auto grp = new Group; - grp._id = BsonObjectID.generate(); - grp.name = name; - grp.description = description; - m_groups.insert(grp); - } + abstract void addGroup(string name, string description); } class User { - BsonObjectID _id; + string id; bool active; bool banned; string name; @@ -302,10 +223,48 @@ class User { string[] groups; string activationCode; string resetCode; - BsonDate resetCodeExpireTime; + SysTime resetCodeExpireTime; AuthInfo auth; Bson[string] properties; + Bson toBson() const + { + Bson[string] props; + props["_id"] = BsonObjectID.fromString(id); + props["active"] = Bson(active); + props["banned"] = Bson(banned); + props["name"] = Bson(name); + props["fullName"] = Bson(fullName); + props["email"] = Bson(email); + props["groups"] = serializeToBson(groups); + props["activationCode"] = Bson(activationCode); + props["resetCode"] = Bson(resetCode); + props["resetCodeExpireTime"] = BsonDate(resetCodeExpireTime); + props["auth"] = serializeToBson(auth); + props["properties"] = serializeToBson(properties); + + return Bson(props); + } + + static User fromBson(Bson src) + { + auto usr = new User; + usr.id = src["_id"].get!BsonObjectID().toString(); + usr.active = src["active"].get!bool; + usr.banned = src["banned"].get!bool; + usr.name = src["name"].get!string; + usr.fullName = src["fullName"].get!string; + usr.email = src["email"].get!string; + usr.groups = deserializeBson!(string[])(src["groups"]); + usr.activationCode = src["activationCode"].get!string; + usr.resetCode = src["resetCode"].get!string; + usr.resetCodeExpireTime = src["resetCodeExpireTime"].get!BsonDate().toSysTime(); + usr.auth = deserializeBson!AuthInfo(src["auth"]); + usr.properties = deserializeBson!(Bson[string])(src["properties"]); + + return usr; + } + bool isInGroup(string name) const { return groups.countUntil(name) >= 0; } } @@ -318,9 +277,29 @@ struct AuthInfo { } class Group { - BsonObjectID _id; + string id; string name; string description; + + Bson toBson() const + { + Bson[string] props; + props["_id"] = Bson(BsonObjectID.fromString(id)); + props["name"] = Bson(name); + props["description"] = Bson(description); + + return Bson(props); + } + + static Group fromBson(Bson src) + { + auto grp = new Group; + grp.id = src["_id"].get!BsonObjectID().toString(); + grp.name = src["name"].get!string; + grp.description = src["description"].get!string; + + return grp; + } } string generateActivationCode() diff --git a/source/userman/mongocontroller.d b/source/userman/mongocontroller.d new file mode 100644 index 0000000..12be86b --- /dev/null +++ b/source/userman/mongocontroller.d @@ -0,0 +1,132 @@ +/** + Database abstraction layer + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module userman.mongocontroller; + +import userman.controller; + +import vibe.db.mongo.mongo; + +import std.string; + + +class MongoController : UserManController { + private { + MongoCollection m_users; + MongoCollection m_groups; + } + + this(UserManSettings settings) + { + super(settings); + + string database = "admin"; + MongoClientSettings dbSettings; + if (parseMongoDBUrl(dbSettings, settings.databaseURL)) + database = dbSettings.database; + + auto db = connectMongoDB(settings.databaseURL).getDatabase(database); + m_users = db["userman.users"]; + m_groups = db["userman.groups"]; + + m_users.ensureIndex(["name": 1], IndexFlags.Unique); + m_users.ensureIndex(["email": 1], IndexFlags.Unique); + } + + override bool isEmailRegistered(string email) + { + auto bu = m_users.findOne(["email": email], ["auth": true]); + return !bu.isNull() && bu.auth.method.get!string.length > 0; + } + + override void addUser(User usr) + { + validateUser(usr); + enforce(m_users.findOne(["name": usr.name]).isNull(), "The user name is already taken."); + enforce(m_users.findOne(["email": usr.email]).isNull(), "The email address is already in use."); + + usr.id = BsonObjectID.generate().toString(); + m_users.insert(usr); + } + + override User getUser(string id) + { + auto busr = m_users.findOne(["_id": BsonObjectID.fromString(id)]); + enforce(!busr.isNull(), "The specified user id is invalid."); + auto ret = new User; + deserializeBson(ret, busr); + return ret; + } + + override User getUserByName(string name) + { + name = name.toLower(); + + auto busr = m_users.findOne(["name": name]); + enforce(!busr.isNull(), "The specified user name is not registered."); + auto ret = new User; + deserializeBson(ret, busr); + return ret; + } + + override User getUserByEmail(string email) + { + email = email.toLower(); + + auto busr = m_users.findOne(["email": email]); + enforce(!busr.isNull(), "There is no user account for the specified email address."); + auto ret = new User; + deserializeBson(ret, busr); + return ret; + } + + override User getUserByEmailOrName(string email_or_name) + { + auto busr = m_users.findOne(["$or": [["email": email_or_name.toLower()], ["name": email_or_name]]]); + enforce(!busr.isNull(), "The specified email address or user name is not registered."); + auto ret = new User; + deserializeBson(ret, busr); + return ret; + } + + override void enumerateUsers(int first_user, int max_count, void delegate(ref User usr) del) + { + foreach( busr; m_users.find(["query": null, "orderby": ["name": 1]], null, QueryFlags.None, first_user, max_count) ){ + if (max_count-- <= 0) break; + auto usr = deserializeBson!User(busr); + del(usr); + } + } + + override long getUserCount() + { + return m_users.count(Bson.emptyObject); + } + + override void deleteUser(string user_id) + { + m_users.remove(["_id": BsonObjectID.fromString(user_id)]); + } + + override void updateUser(User user) + { + validateUser(user); + enforce(m_settings.useUserNames || user.name == user.email, "User name must equal email address if user names are not used."); + + m_users.update(["_id": user.id], user); + } + + override void addGroup(string name, string description) + { + enforce(m_groups.findOne(["name": name]).isNull(), "A group with this name already exists."); + auto grp = new Group; + grp.id = BsonObjectID.generate().toString(); + grp.name = name; + grp.description = description; + m_groups.insert(grp); + } +} diff --git a/source/userman/rediscontroller.d b/source/userman/rediscontroller.d new file mode 100644 index 0000000..7169256 --- /dev/null +++ b/source/userman/rediscontroller.d @@ -0,0 +1,290 @@ +/** + Database abstraction layer + + Copyright: © 2012 RejectedSoftware e.K. + License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module userman.rediscontroller; + +import userman.controller; + +import vibe.db.redis.redis; + +import std.datetime; +import std.exception; +import std.string; +import std.conv; + + +class RedisController : UserManController { + private { + RedisClient m_redisClient; + RedisDatabase m_redisDB; + } + + this(UserManSettings settings) + { + super(settings); + + string schema = "redis"; + auto idx = settings.databaseURL.indexOf("://"); + if (idx > 0) + schema = settings.databaseURL[0..idx]; + + enforce(schema == "redis", "databaseURL must be a redis connection string"); + + // Parse string by replacing schema with 'http' as URL won't parse redis + // URLs correctly. + string url_string = settings.databaseURL; + if (idx > 0) + url_string = url_string[idx+3..$]; + + URL url = URL("http://" ~ url_string); + url.schema = "redis"; + + long dbIndex = 0; + if (!url.path.empty) + dbIndex = to!long(url.path.nodes[0].toString()); + + m_redisClient = connectRedis(url.host, url.port == ushort.init ? 6379 : url.port); + m_redisDB = m_redisClient.getDatabase(dbIndex); + } + + override bool isEmailRegistered(string email) + { + string userId = m_redisDB.get!string("userman:email_user:" ~ email); + if (userId != string.init){ + string method = m_redisDB.hget!string(format("userman:user:%s:auth"), "method"); + return method != string.init && method.length > 0; + } + return false; + } + + override void addUser(User usr) + { + validateUser(usr); + + enforce(m_redisDB.get!string("userman:name_user:" ~ usr.name) == string.init, "The user name is already taken."); + enforce(m_redisDB.get!string("userman:email_user:" ~ usr.email) == string.init, "The email address is already in use."); + + long userId = m_redisDB.incr("userman:nextUserId"); + usr.id = to!string(userId); + + // Indexes + m_redisDB.zadd("userman:users", userId, userId); + if (usr.email != string.init) m_redisDB.set("userman:email_user:" ~ usr.email, to!string(userId)); + if (usr.name != string.init) m_redisDB.set("userman:name_user:" ~ usr.name, to!string(userId)); + + // User + m_redisDB.hmset(format("userman:user:%s", userId), + "active", to!string(usr.active), + "banned", to!string(usr.banned), + "name", usr.name, + "fullName", usr.fullName, + "email", usr.email, + "activationCode", usr.activationCode, + "resetCode", usr.resetCode, + "resetCodeExpireTime", usr.resetCodeExpireTime == SysTime() ? "" : usr.resetCodeExpireTime.toISOExtString()); + + // Credentials + m_redisDB.hmset(format("userman:user:%s:auth", userId), + "method", usr.auth.method, + "passwordHash", usr.auth.passwordHash, + "token", usr.auth.token, + "secret", usr.auth.secret, + "info", usr.auth.info); + + foreach(string group; usr.groups) + m_redisDB.sadd("userman:group:" ~ group ~ ":members", userId); + } + + override User getUser(string id) + { + auto userHash = m_redisDB.hgetAll("userman:user:" ~ id); + enforce(userHash.hasNext(), "The specified user id is invalid."); + + auto ret = new User; + + ret.id = id; + while (userHash.hasNext()) { + string key = userHash.next!string(); + string value = userHash.next!string(); + switch (key) + { + case "active": ret.active = to!bool(value); break; + case "banned": ret.banned = to!bool(value); break; + case "name": ret.name = value; break; + case "fullName": ret.fullName = value; break; + case "email": ret.email = value; break; + case "activationCode": ret.activationCode = value; break; + case "resetCode": ret.resetCode = value; break; + case "resetCodeExpireTime": + try { + ret.resetCodeExpireTime = SysTime.fromISOExtString(value); + } catch (DateTimeException dte) {} + break; + default: break; + } + } + + auto authHash = m_redisDB.hgetAll(format("userman:user:%s:auth", id)); + if(authHash.hasNext()) { + AuthInfo auth; + while (authHash.hasNext()) { + string key = authHash.next!string(); + string value = authHash.next!string(); + switch (key) + { + case "method": auth.method = value; break; + case "passwordHash": auth.passwordHash = value; break; + case "token": auth.token = value; break; + case "secret": auth.secret = value; break; + case "info": auth.info = value; break; + default: break; + } + } + ret.auth = auth; + } + + auto groupNames = m_redisDB.zrange("userman:groups", 0, -1); + while (groupNames.hasNext()) { + string name = groupNames.next!string(); + if (m_redisDB.sisMember("userman:group:" ~ name ~ ":members", id)) + { + ++ret.groups.length; + ret.groups[ret.groups.length - 1] = name; + } + } + + return ret; + } + + override User getUserByName(string name) + { + name = name.toLower(); + + string userId = m_redisDB.get!string("userman:name_user:" ~ name); + try { + return getUser(userId); + } + catch (Exception e) { + throw new Exception("The specified user name is not registered."); + } + } + + override User getUserByEmail(string email) + { + email = email.toLower(); + + string userId = m_redisDB.get!string("userman:email_user:" ~ email); + try { + return getUser(userId); + } + catch (Exception e) { + throw new Exception("There is no user account for the specified email address."); + } + } + + override User getUserByEmailOrName(string email_or_name) + { + string userId = m_redisDB.get!string("userman:email_user:" ~ email_or_name.toLower()); + if (userId == null) + userId = m_redisDB.get!string("userman:name_user:" ~ email_or_name); + + try { + return getUser(userId); + } + catch (Exception e) { + throw new Exception("The specified email address or user name is not registered."); + } + } + + override void enumerateUsers(int first_user, int max_count, void delegate(ref User usr) del) + { + auto userIds = m_redisDB.zrange("userman:users", first_user, first_user + max_count); + while (userIds.hasNext()) { + auto usr = getUser(userIds.next!string()); + del(usr); + } + } + + override long getUserCount() + { + return m_redisDB.zcard("userman:users"); + } + + override void deleteUser(string user_id) + { + User usr = getUser(user_id); + + if (usr !is null) + { + // Indexes + m_redisDB.zrem("userman:users", user_id); + m_redisDB.del("userman:email_user:" ~ usr.email); + m_redisDB.del("userman:name_user:" ~ usr.name); + + // User + m_redisDB.del(format("userman:user:%s", user_id)); + + // Credentials + m_redisDB.del(format("userman:user:%s:auth", user_id)); + + // Group membership + foreach(string group; usr.groups) + m_redisDB.srem("userman:group:" ~ group ~ ":members", user_id); + } + } + + override void updateUser(User user) + { + validateUser(user); + enforce(m_settings.useUserNames || user.name == user.email, "User name must equal email address if user names are not used."); + + // User + m_redisDB.hmset(format("userman:user:%s", user.id), + "active", to!string(user.active), + "banned", to!string(user.banned), + "name", user.name, + "fullName", user.fullName, + "email", user.email, + "activationCode", user.activationCode, + "resetCode", user.resetCode, + "resetCodeExpireTime", user.resetCodeExpireTime == SysTime() ? "" : user.resetCodeExpireTime.toISOExtString()); + + // Credentials + m_redisDB.hmset(format("userman:user:%s:auth", user.id), + "method", user.auth.method, + "passwordHash", user.auth.passwordHash, + "token", user.auth.token, + "secret", user.auth.secret, + "info", user.auth.info); + + + auto groupNames = m_redisDB.zrange("userman:groups", 0, -1); + while (groupNames.hasNext()) { + string name = groupNames.next!string(); + if (user.isInGroup(name)) + m_redisDB.sadd("userman:group:" ~ name ~ ":members", user.id); + else + m_redisDB.srem("userman:group:" ~ name ~ ":members", user.id); + } + } + + override void addGroup(string name, string description) + { + enforce(!m_redisDB.hexists("userman:groups", name), "A group with this name already exists."); + + long groupId = m_redisDB.incr("userman:nextGroupId"); + + // Index + m_redisDB.zadd("userman:groups", groupId, name); + + // Group + m_redisDB.hmset(format("userman:group:%s", groupId), + "name", name, + "description", description); + + } +} diff --git a/source/userman/userman.d b/source/userman/userman.d index b140d08..28df2c6 100644 --- a/source/userman/userman.d +++ b/source/userman/userman.d @@ -13,7 +13,7 @@ public import vibe.inet.url; class UserManSettings { bool requireAccountValidation = true; bool useUserNames = true; // use a user name or the email address for identification? - string databaseName = "test"; + string databaseURL = "mongodb://127.0.0.1:27017/test";//*/"redis://127.0.0.1:6379/1"; string serviceName = "User database test"; URL serviceUrl = "http://www.example.com/"; string serviceEmail = "userdb@example.com"; diff --git a/source/userman/web.d b/source/userman/web.d index 4f23cd0..c2cb3a6 100644 --- a/source/userman/web.d +++ b/source/userman/web.d @@ -244,7 +244,7 @@ class UserManWebInterface { m_sessUserEmail = user.email; m_sessUserName = user.name; m_sessUserFullName = user.fullName; - m_sessUserID = user._id.toString(); + m_sessUserID = user.id; .redirect(redirect.length ? redirect : m_prefix); } @@ -306,7 +306,7 @@ class UserManWebInterface { m_sessUserEmail = user.email; m_sessUserName = user.name; m_sessUserFullName = user.fullName; - m_sessUserID = user._id.toString(); + m_sessUserID = user.id; render!("userman.activate.dt"); }