plz / user / user.v
473 lines · 394 sloc · 11.31 KB · d4104e7627c1594b9de8e026959b196932f293c8
Raw
1module main
2
3import crypto.sha256
4import time
5import os
6
7struct User {
8 id int @[primary; sql: serial]
9 full_name string
10 username string @[unique]
11 github_username string
12 password string
13 salt string
14 created_at time.Time
15 is_github bool
16 is_registered bool
17 is_blocked bool
18 is_admin bool
19 oauth_state string @[skip]
20mut:
21 // for github oauth XSRF protection
22 namechanges_count int
23 last_namechange_time int
24 posts_count int
25 last_post_time int
26 avatar string
27 emails []Email @[skip]
28 login_attempts int
29}
30
31struct Email {
32 id int @[primary; sql: serial]
33 user_id int
34 email string @[unique]
35}
36
37struct Contributor {
38 id int @[primary; sql: serial]
39 user_id int @[unique: 'contributor']
40 repo_id int @[unique: 'contributor']
41}
42
43pub fn (mut app App) set_user_block_status(user_id int, status bool) ! {
44 sql app.db {
45 update User set is_blocked = status where id == user_id
46 }!
47}
48
49pub fn (mut app App) set_user_admin_status(user_id int, status bool) ! {
50 sql app.db {
51 update User set is_admin = status where id == user_id
52 }!
53}
54
55fn hash_password_with_salt(password string, salt string) string {
56 salted_password := '${password}${salt}'
57
58 return sha256.sum(salted_password.bytes()).hex().str()
59}
60
61fn compare_password_with_hash(password string, salt string, hashed string) bool {
62 return hash_password_with_salt(password, salt) == hashed
63}
64
65pub fn (mut app App) register_user(username string, password string, salt string, emails []string, github bool, is_admin bool) !bool {
66 mut user := app.get_user_by_username(username) or { User{} }
67
68 if user.id != 0 && user.is_registered {
69 app.info('User ${username} already exists')
70 return error('username `${username}` is already taken')
71 }
72
73 // A non-registered row with this username exists (e.g. a GitHub shadow user).
74 // Block normal registration; the GitHub flow handles upgrading shadow users itself.
75 if user.id != 0 && !github {
76 app.info('Username ${username} is reserved by an unregistered/shadow user')
77 return error('username `${username}` is already taken')
78 }
79
80 user = app.get_user_by_email(emails[0]) or { User{} }
81
82 if user.id != 0 && user.is_registered {
83 app.info('Email ${emails[0]} is already in use')
84 return error('email `${emails[0]}` is already in use')
85 }
86
87 if user.id == 0 {
88 // Final guard: make sure no Email row points at this address even if
89 // the parent user lookup didn't surface (orphaned/duplicate rows).
90 if app.email_exists(emails[0]) {
91 return error('email `${emails[0]}` is already in use')
92 }
93
94 user = User{
95 username: username
96 password: password
97 salt: salt
98 created_at: time.now()
99 is_registered: true
100 is_github: github
101 github_username: username
102 avatar: default_avatar_name
103 is_admin: is_admin
104 }
105
106 app.add_user(user) or {
107 if is_unique_constraint_error(err) {
108 return error('username `${username}` or email `${emails[0]}` is already in use')
109 }
110 return err
111 }
112
113 mut u := app.get_user_by_username(user.username) or {
114 app.info('User was not inserted')
115 return error('user `${username}` was not inserted (lookup after insert failed: ${err})')
116 }
117
118 if u.password != user.password {
119 app.info('User was not inserted (password mismatch after insert)')
120 return error('user `${username}` was not inserted (password mismatch after insert)')
121 }
122 if u.username != user.username {
123 app.info('User was not inserted (username mismatch after insert)')
124 return error('user `${username}` was not inserted (username mismatch after insert: got `${u.username}`)')
125 }
126
127 app.add_activity(u.id, 'joined')!
128
129 for email in emails {
130 app.add_email(u.id, email) or {
131 if is_unique_constraint_error(err) {
132 return error('email `${email}` is already in use')
133 }
134 return err
135 }
136 }
137
138 u.emails = app.find_user_emails(u.id)
139 } else {
140 // Update existing user
141 if !github {
142 app.create_user_dir(username)
143
144 return true
145 }
146
147 if user.is_registered {
148 sql app.db {
149 update User set is_github = true where id == user.id
150 }!
151 return true
152 }
153 }
154 app.create_user_dir(username)
155
156 return true
157}
158
159fn is_unique_constraint_error(err IError) bool {
160 return err.msg().to_lower().contains('unique constraint')
161}
162
163pub fn (app App) email_exists(value string) bool {
164 rows := sql app.db {
165 select from Email where email == value limit 1
166 } or { [] }
167 return rows.len > 0
168}
169
170fn (mut app App) create_user_dir(username string) {
171 user_path := '${app.config.repo_storage_path}/${username}'
172
173 os.mkdir(user_path) or {
174 app.info('Failed to create ${user_path}')
175 app.info('Error: ${err}')
176 return
177 }
178}
179
180pub fn (mut app App) update_user_avatar(user_id int, filename_or_url string) ! {
181 sql app.db {
182 update User set avatar = filename_or_url where id == user_id
183 }!
184}
185
186pub fn (mut app App) add_user(user User) ! {
187 sql app.db {
188 insert user into User
189 }!
190}
191
192pub fn (mut app App) add_email(user_id int, email string) ! {
193 user_email := Email{
194 user_id: user_id
195 email: email
196 }
197
198 sql app.db {
199 insert user_email into Email
200 }!
201}
202
203pub fn (mut app App) add_contributor(user_id int, repo_id int) ! {
204 if !app.contains_contributor(user_id, repo_id) {
205 contributor := Contributor{
206 user_id: user_id
207 repo_id: repo_id
208 }
209
210 sql app.db {
211 insert contributor into Contributor
212 }!
213 }
214}
215
216pub fn (app App) get_username_by_id(id int) ?string {
217 users := sql app.db {
218 select from User where id == id limit 1
219 } or { [] }
220
221 if users.len == 0 {
222 return none
223 }
224
225 return users.first().username
226}
227
228pub fn (app App) get_user_by_username(value string) ?User {
229 users := sql app.db {
230 select from User where username == value limit 1
231 } or { [] }
232
233 if users.len == 0 {
234 return none
235 }
236
237 mut user := users.first()
238 emails := app.find_user_emails(user.id)
239 user.emails = emails
240
241 return user
242}
243
244pub fn (app App) get_user_by_id(id int) ?User {
245 users := sql app.db {
246 select from User where id == id
247 } or { [] }
248
249 if users.len == 0 {
250 return none
251 }
252
253 mut user := users.first()
254 emails := app.find_user_emails(user.id)
255 user.emails = emails
256
257 return user
258}
259
260pub fn (mut app App) get_user_by_github_username(name string) ?User {
261 users := sql app.db {
262 select from User where github_username == name limit 1
263 } or { [] }
264
265 if users.len == 0 {
266 return none
267 }
268
269 mut user := users.first()
270 emails := app.find_user_emails(user.id)
271 user.emails = emails
272
273 return user
274}
275
276pub fn (mut app App) get_user_by_email(value string) ?User {
277 emails := sql app.db {
278 select from Email where email == value
279 } or { [] }
280
281 if emails.len != 1 {
282 return none
283 }
284
285 return app.get_user_by_id(emails[0].user_id)
286}
287
288pub fn (app App) find_user_emails(user_id int) []Email {
289 emails := sql app.db {
290 select from Email where user_id == user_id
291 } or { [] }
292
293 return emails
294}
295
296pub fn (mut app App) find_repo_registered_contributor(id int) []User {
297 contributors := sql app.db {
298 select from Contributor where repo_id == id
299 } or { [] }
300 mut users := []User{cap: contributors.len}
301 for contributor in contributors {
302 user := app.get_user_by_id(contributor.user_id) or { continue }
303
304 users << user
305 }
306 return users
307}
308
309pub fn (mut app App) get_all_registered_users_as_page(offset int) []User {
310 // FIXME: 30 -> admin_users_per_page
311 mut users := sql app.db {
312 select from User where is_registered == true limit 30 offset offset
313 } or { [] }
314 for i, user in users {
315 users[i].emails = app.find_user_emails(user.id)
316 }
317 return users
318}
319
320pub fn (mut app App) get_all_registered_user_count() int {
321 return sql app.db {
322 select count from User where is_registered == true
323 } or { 0 }
324}
325
326fn (mut app App) search_users(query string) []User {
327 q :=
328 'select id, full_name, username, avatar from ${sql_table('User')} where is_blocked is false and ' +
329 '(username like ${sql_like_pattern(query)} or full_name like ${sql_like_pattern(query)})'
330 repo_rows := db_exec_values(mut app.db, q) or { return [] }
331 mut users := []User{}
332 for row in repo_rows {
333 users << User{
334 id: row[0].int()
335 full_name: row[1]
336 username: row[2]
337 avatar: row[3]
338 }
339 }
340 return users
341}
342
343pub fn (mut app App) get_users_count() !int {
344 return sql app.db {
345 select count from User
346 }!
347}
348
349pub fn (mut app App) get_count_repo_contributors(id int) !int {
350 return sql app.db {
351 select count from Contributor where repo_id == id
352 } or { 0 }
353}
354
355pub fn (mut app App) contains_contributor(user_id int, repo_id int) bool {
356 count := sql app.db {
357 select count from Contributor where repo_id == repo_id && user_id == user_id
358 } or { 0 }
359 return count > 0
360}
361
362pub fn (mut app App) increment_user_post(mut user User) ! {
363 user.posts_count++
364
365 u := *user
366 id := u.id
367 now := int(time.now().unix())
368 lastplus := int(time.unix(u.last_post_time).add_days(1).unix())
369
370 if now >= lastplus {
371 user.last_post_time = now
372 sql app.db {
373 update User set posts_count = 0, last_post_time = now where id == id
374 }!
375 }
376
377 sql app.db {
378 update User set posts_count = posts_count + 1 where id == id
379 }!
380}
381
382pub fn (mut app App) increment_user_login_attempts(user_id int) ! {
383 sql app.db {
384 update User set login_attempts = login_attempts + 1 where id == user_id
385 }!
386}
387
388pub fn (mut app App) update_user_login_attempts(user_id int, attempts int) ! {
389 sql app.db {
390 update User set login_attempts = attempts where id == user_id
391 }!
392}
393
394pub fn (mut app App) check_user_blocked(user_id int) bool {
395 user := app.get_user_by_id(user_id) or { return false }
396 return user.is_blocked
397}
398
399fn (mut app App) change_username(user_id int, username string) ! {
400 sql app.db {
401 update User set username = username where id == user_id
402 }!
403
404 sql app.db {
405 update Repo set user_name = username where user_id == user_id
406 }!
407}
408
409fn (mut app App) change_full_name(user_id int, full_name string) ! {
410 sql app.db {
411 update User set full_name = full_name where id == user_id
412 }!
413}
414
415fn (mut app App) incement_namechanges(user_id int) ! {
416 now := int(time.now().unix())
417 sql app.db {
418 update User set namechanges_count = namechanges_count + 1, last_namechange_time = now
419 where id == user_id
420 }!
421}
422
423fn (mut app App) check_username(username string) (bool, User) {
424 if username.len == 0 {
425 return false, User{}
426 }
427 mut user := app.get_user_by_username(username) or { return false, User{} }
428 return user.is_registered, user
429}
430
431pub fn (mut app App) auth_user(mut ctx Context, user User, ip string) ! {
432 token := app.add_token(user.id, ip)!
433 app.update_user_login_attempts(user.id, 0)!
434 expire_date := time.now().add_days(200)
435 ctx.set_cookie(name: 'token', value: token, expires: expire_date)
436}
437
438pub fn (mut app App) is_logged_in(mut ctx Context) bool {
439 token_cookie := ctx.get_cookie('token') or { return false }
440 token := app.get_token(token_cookie) or { return false }
441 is_user_blocked := app.check_user_blocked(token.user_id)
442 if is_user_blocked {
443 app.handle_logout(mut ctx)
444 return false
445 }
446 return true
447}
448
449pub fn (mut app App) get_user_from_cookies(ctx &Context) ?User {
450 token_cookie := ctx.get_cookie('token') or { return none }
451 token := app.get_token(token_cookie) or { return none }
452 mut user := app.get_user_by_id(token.user_id) or { return none }
453 return user
454}
455
456// activity_level maps a per-day commit count to a heatmap intensity level 0..4,
457// scaled by the user's busiest day across the window.
458fn activity_level(count int, max int) int {
459 if count <= 0 || max <= 0 {
460 return 0
461 }
462 ratio := f64(count) / f64(max)
463 if ratio > 0.75 {
464 return 4
465 }
466 if ratio > 0.5 {
467 return 3
468 }
469 if ratio > 0.25 {
470 return 2
471 }
472 return 1
473}
474