1 /++
2   A module containing a class that handles users data
3 
4   Copyright: © 2018-2020 Szabo Bogdan
5   License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6   Authors: Szabo Bogdan
7 +/
8 
9 module vibeauth.data.user;
10 
11 import vibeauth.data.usermodel;
12 import vibeauth.data.token;
13 
14 import std.datetime;
15 import std.conv;
16 import std.algorithm;
17 import std.uuid;
18 import std.array;
19 
20 import vibe.crypto.cryptorand;
21 
22 import vibe.data.json;
23 
24 /// Class used to manage one user
25 class User {
26 
27   /// Event type raised when the user data has changed
28   alias ChangedEvent = void delegate(User);
29 
30   /// Event raised when the user changed
31   ChangedEvent onChange;
32 
33   private {
34     UserModel userData;
35   }
36 
37   ///
38   this() { }
39 
40   ///
41   this(UserModel userData) {
42     this.userData = userData;
43   }
44 
45   ///
46   this(string email, string password) {
47     this.userData.email = email;
48     setPassword(password);
49   }
50 
51   /// Convert the user object ot a Json pretty string
52   override string toString() {
53     return toJson.toPrettyString;
54   }
55 
56   @property {
57     /// Get the user id
58     auto id() const {
59       return userData._id;
60     }
61 
62     /// Set the user id
63     void id(ulong value) {
64       userData._id = value.to!string;
65       userData.lastActivity = Clock.currTime.toUnixTime!long;
66 
67       if(onChange) {
68         onChange(this);
69       }
70     }
71 
72     /// Check if the user is active
73     bool isActive() const {
74       return userData.isActive;
75     }
76 
77     /// Check the user active status
78     void isActive(bool value) {
79       userData.isActive = value;
80       userData.lastActivity = Clock.currTime.toUnixTime!long;
81 
82       if(onChange) {
83         onChange(this);
84       }
85     }
86 
87     /// Get the user email
88     string email() const {
89       return userData.email;
90     }
91 
92     /// Set the user email
93     void email(string value) {
94       userData.email = value;
95       userData.lastActivity = Clock.currTime.toUnixTime!long;
96 
97       if(onChange) {
98         onChange(this);
99       }
100     }
101 
102     /// Get the user title
103     auto salutation() const {
104       return userData.salutation;
105     }
106 
107     /// Set the user title
108     void salutation(string value) {
109       userData.salutation = value;
110       userData.lastActivity = Clock.currTime.toUnixTime!long;
111 
112       if(onChange) {
113         onChange(this);
114       }
115     }
116 
117     /// Get the user title
118     auto title() const {
119       return userData.title;
120     }
121 
122     /// Set the user title
123     void title(string value) {
124       userData.title = value;
125       userData.lastActivity = Clock.currTime.toUnixTime!long;
126 
127       if(onChange) {
128         onChange(this);
129       }
130     }
131 
132     /// Get the user first name
133     auto firstName() const {
134       return userData.firstName;
135     }
136 
137     /// Set the user first name
138     void firstName(string value) {
139       userData.firstName = value;
140       userData.lastActivity = Clock.currTime.toUnixTime!long;
141 
142       if(onChange) {
143         onChange(this);
144       }
145     }
146 
147     /// Get the user last name
148     auto lastName() const {
149       return userData.lastName;
150     }
151 
152     /// Set the user last name
153     void lastName(string value) {
154       userData.lastName = value;
155       userData.lastActivity = Clock.currTime.toUnixTime!long;
156 
157       if(onChange) {
158         onChange(this);
159       }
160     }
161 
162     /// Get the user alias name
163     auto username() const {
164       return userData.username;
165     }
166 
167     /// Set the user alias name
168     void username(string value) {
169       userData.username = value;
170       userData.lastActivity = Clock.currTime.toUnixTime!long;
171 
172       if(onChange) {
173         onChange(this);
174       }
175     }
176 
177     /// Get the last user activity timestamp
178     auto lastActivity() const {
179       return userData.lastActivity;
180     }
181 
182     /// Set the last user activity timestam[]
183     void lastActivity(ulong value) {
184       userData.lastActivity = value;
185 
186       if(onChange) {
187         onChange(this);
188       }
189     }
190   }
191 
192   /// Revoke a token
193   void revoke(string token) {
194     userData.tokens = userData.tokens.filter!(a => a.name != token).array;
195     userData.lastActivity = Clock.currTime.toUnixTime!long;
196 
197     if(onChange) {
198       onChange(this);
199     }
200   }
201 
202   const {
203     /// Get the user scopes assigned to a particullar token
204     string[] getScopes(string token) {
205       return userData.tokens.filter!(a => a.name == token).front.scopes.to!(string[]);
206     }
207 
208     /// Get all user scopes
209     string[] getScopes() {
210       return userData.scopes.dup;
211     }
212 
213     /// Check if an user can access a scope
214     bool can(string access)() {
215       return userData.scopes.canFind(access);
216     }
217 
218     /// Get a range of tokens of a certain type
219     auto getTokensByType(string type) {
220       auto now = Clock.currTime;
221       return userData.tokens.filter!(a => a.type == type && a.expire > now);
222     }
223 
224     /// Validate a password
225     bool isValidPassword(string password) {
226       return sha1UUID(userData.salt ~ "." ~ password).to!string == userData.password;
227     }
228 
229     /// Validate a token
230     bool isValidToken(string token) {
231       auto now = Clock.currTime;
232       return userData.tokens.filter!(a => a.expire > now).map!(a => a.name).canFind(token);
233     }
234 
235     /// Validate a token against a scope
236     bool isValidToken(string token, string requiredScope) {
237       auto now = Clock.currTime;
238       return userData.tokens.filter!(a => a.scopes.canFind(requiredScope) && a.expire > now).map!(a => a.name).canFind(token);
239     }
240   }
241 
242   void removeExpiredTokens() {
243     auto now = Clock.currTime;
244     auto newTokenList = userData.tokens.filter!(a => a.expire > now).array;
245 
246     if(newTokenList.length != userData.tokens.length) {
247       userData.tokens = newTokenList;
248 
249       if(onChange) {
250         onChange(this);
251       }
252     }
253   }
254 
255   /// Change the user password
256   void setPassword(string password) {
257     ubyte[16] secret;
258     secureRNG.read(secret[]);
259     auto uuid = UUID(secret);
260 
261     userData.salt = uuid.to!string;
262     userData.password = sha1UUID(userData.salt ~ "." ~ password).to!string;
263     userData.lastActivity = Clock.currTime.toUnixTime!long;
264 
265     if(onChange) {
266       onChange(this);
267     }
268   }
269 
270   /// Change the user password by providing a salting string
271   void setPassword(string password, string salt) {
272     userData.salt = salt;
273     userData.password = password;
274     userData.lastActivity = Clock.currTime.toUnixTime!long;
275 
276     if(onChange) {
277       onChange(this);
278     }
279   }
280 
281   /// Add a scope to the user
282   void addScope(string access) {
283     userData.scopes ~= access;
284     userData.lastActivity = Clock.currTime.toUnixTime!long;
285 
286     if(onChange) {
287       onChange(this);
288     }
289   }
290 
291   /// Remove a scope from user
292   void removeScope(string access) {
293     userData.scopes = userData.scopes
294       .filter!(a => a != access).array;
295     userData.lastActivity = Clock.currTime.toUnixTime!long;
296 
297     if(onChange) {
298       onChange(this);
299     }
300   }
301 
302   /// Create an user token
303   Token createToken(SysTime expire, string[] scopes = [], string type = "Bearer", string[string] meta = null) {
304     ubyte[16] secret;
305     secureRNG.read(secret[]);
306     auto uuid = UUID(secret);
307 
308     auto token = Token(uuid.to!string, expire, scopes, type, meta);
309     userData.tokens ~= token;
310 
311     if(onChange) {
312       onChange(this);
313     }
314 
315     return token;
316   }
317 
318   /// Convert the object to a json. It's not safe to share this value
319   /// with the outside world. Use it to store the user to db.
320   Json toJson() const {
321     auto result = userData.serializeToJson;
322 
323     result.remove("name");
324 
325     return result;
326   }
327 
328   /// Convert the object to a json that can be shared with the outside world
329   Json toPublicJson() const {
330     Json data = Json.emptyObject;
331 
332     data["id"] = id;
333     data["salutation"] = salutation;
334     data["title"] = title;
335     data["firstName"] = firstName;
336     data["lastName"] = lastName;
337     data["username"] = username;
338     data["email"] = email;
339     data["lastActivity"] = lastActivity;
340     data["scopes"] = Json.emptyArray;
341 
342     foreach(s; userData.scopes) {
343       data["scopes"] ~= s;
344     }
345 
346     return data;
347   }
348 
349   /// Restore the user from a json value
350   static User fromJson(Json data) {
351     if(data["lastActivity"].type != Json.Type.Int) {
352       data["lastActivity"] = 0;
353     }
354 
355     if(data["name"].type == Json.Type..string && data["name"] != "") {
356       data["firstName"] = data["name"];
357     }
358 
359     return new User(data.deserializeJson!UserModel);
360   }
361 }