plz / user / user_routes.v
356 lines · 285 sloc · 10.33 KB · 937c8e999c24a3e84663b9b84629a6fab6ff2114
Raw
1module main
2
3import time
4import os
5import veb
6import rand
7import validation
8import api
9
10pub fn (mut app App) login(mut ctx Context) veb.Result {
11 csrf := rand.string(30)
12 ctx.set_cookie(name: 'csrf', value: csrf)
13
14 if app.is_logged_in(mut ctx) {
15 return ctx.redirect('/' + ctx.user.username)
16 }
17
18 return $veb.html()
19}
20
21@['/login'; post]
22pub fn (mut app App) handle_login(mut ctx Context, username string, password string) veb.Result {
23 if username == '' || password == '' {
24 return ctx.redirect_to_login()
25 }
26 user := app.get_user_by_username(username) or { return ctx.redirect_to_login() }
27 if user.is_blocked {
28 return ctx.redirect_to_login()
29 }
30 if !compare_password_with_hash(password, user.salt, user.password) {
31 app.increment_user_login_attempts(user.id) or {
32 ctx.error('There was an error while logging in')
33 return app.login(mut ctx)
34 }
35 if user.login_attempts == max_login_attempts {
36 app.warn('User ${user.username} got blocked')
37 app.block_user(user.id) or { app.info(err.str()) }
38 }
39 ctx.error('Wrong username/password')
40 return app.login(mut ctx)
41 }
42 if !user.is_registered {
43 return ctx.redirect_to_login()
44 }
45 if app.user_has_two_factor(user.id) {
46 expires := time.now().unix() + two_factor_pending_ttl
47 token := app.sign_pending_2fa(user, expires)
48 ctx.set_cookie(name: two_factor_pending_cookie, value: token, path: '/')
49 return ctx.redirect('/login/2fa')
50 }
51 app.auth_user(mut ctx, user, ctx.ip()) or {
52 ctx.error('There was an error while logging in')
53 return app.login(mut ctx)
54 }
55 app.add_security_log(user_id: user.id, kind: .logged_in) or { app.info(err.str()) }
56 return ctx.redirect('/${username}')
57}
58
59@['/logout']
60pub fn (mut app App) handle_logout(mut ctx Context) veb.Result {
61 ctx.set_cookie(name: 'token', value: '')
62 return ctx.redirect_to_index()
63}
64
65@['/:username']
66pub fn (mut app App) user(mut ctx Context, username string) veb.Result {
67 exists, user := app.check_username(username)
68 if !exists {
69 return ctx.not_found()
70 }
71 is_page_owner := username == ctx.user.username
72 mut repos := app.find_user_profile_repos(user.id, is_page_owner)
73 for mut repo in repos {
74 repo.lang_stats = app.find_repo_lang_stats(repo.id)
75 repo.latest_commit_at = app.find_repo_last_commit_time(repo.id)
76 }
77 activity_days := 365
78 activity_buckets := app.get_user_daily_activity(user.id, activity_days)
79 mut activity_total := 0
80 mut activity_max := 0
81 for v in activity_buckets {
82 activity_total += v
83 if v > activity_max {
84 activity_max = v
85 }
86 }
87 activity_oldest := time.now().add_days(-(activity_days - 1))
88 // Render as a 7-row grid (Mon top → Sun bottom), columns are weeks.
89 // We need to pad leading cells so the first day lands on its weekday row.
90 activity_leading := activity_oldest.day_of_week() - 1
91 activity_start_label := activity_oldest.md()
92 activity_end_label := time.now().md()
93 activities := app.find_activities(user.id)
94 return $veb.html()
95}
96
97@['/:username/settings']
98pub fn (mut app App) user_settings(mut ctx Context, username string) veb.Result {
99 is_users_settings := username == ctx.user.username
100
101 if !ctx.logged_in || !is_users_settings {
102 return ctx.redirect_to_index()
103 }
104
105 return $veb.html('templates/user/settings.html')
106}
107
108@['/:username/settings'; post]
109pub fn (mut app App) handle_update_user_settings(mut ctx Context, username string) veb.Result {
110 is_users_settings := username == ctx.user.username
111
112 if !ctx.logged_in || !is_users_settings {
113 return ctx.redirect_to_index()
114 }
115
116 // TODO: uneven parameters count (2) in `handle_update_user_settings`, compared to the vweb route `['/:user/settings', 'post']` (1)
117 new_username := ctx.form['name']
118 full_name := ctx.form['full_name']
119
120 is_username_empty := validation.is_string_empty(new_username)
121
122 if is_username_empty {
123 ctx.error('New name is empty')
124
125 return app.user_settings(mut ctx, username)
126 }
127
128 if ctx.user.namechanges_count > max_namechanges {
129 ctx.error('You can not change your username, limit reached')
130
131 return app.user_settings(mut ctx, username)
132 }
133
134 is_username_valid := validation.is_username_valid(new_username)
135
136 if !is_username_valid {
137 ctx.error('New username is not valid')
138
139 return app.user_settings(mut ctx, username)
140 }
141
142 is_first_namechange := ctx.user.last_namechange_time == 0
143 can_change_usernane := ctx.user.last_namechange_time + namechange_period <= time.now().unix()
144
145 if !(is_first_namechange || can_change_usernane) {
146 ctx.error('You need to wait until you can change the name again')
147
148 return app.user_settings(mut ctx, username)
149 }
150
151 is_new_username := new_username != username
152 is_new_full_name := full_name != ctx.user.full_name
153
154 if is_new_full_name {
155 app.change_full_name(ctx.user.id, full_name) or {
156 ctx.error('There was an error while updating the settings')
157 return app.user_settings(mut ctx, username)
158 }
159 }
160
161 if is_new_username {
162 user := app.get_user_by_username(new_username) or { User{} }
163
164 if user.id != 0 {
165 ctx.error('Name already exists')
166
167 return app.user_settings(mut ctx, username)
168 }
169
170 app.change_username(ctx.user.id, new_username) or {
171 ctx.error('There was an error while updating the settings')
172 return app.user_settings(mut ctx, username)
173 }
174 app.incement_namechanges(ctx.user.id) or {
175 ctx.error('There was an error while updating the settings')
176 return app.user_settings(mut ctx, username)
177 }
178 app.rename_user_directory(username, new_username)
179 }
180
181 return ctx.redirect('/${new_username}')
182}
183
184fn (mut app App) rename_user_directory(old_name string, new_name string) {
185 os.mv('${app.config.repo_storage_path}/${old_name}',
186 '${app.config.repo_storage_path}/${new_name}') or { panic(err) }
187}
188
189pub fn (mut app App) register(mut ctx Context) veb.Result {
190 if ctx.logged_in {
191 return ctx.redirect('/${ctx.user.username}')
192 }
193 csrf := rand.string(30)
194 ctx.set_cookie(name: 'csrf', value: csrf)
195
196 user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) }
197 no_users := user_count == 0
198
199 ctx.current_path = ''
200
201 return $veb.html()
202}
203
204fn (mut app App) register_failed(mut ctx Context, no_redirect string, msg string) veb.Result {
205 if no_redirect == '1' {
206 ctx.res.set_status(.bad_request)
207 return ctx.text(msg)
208 }
209 ctx.error(msg)
210 return app.register(mut ctx)
211}
212
213@['/register'; post]
214pub fn (mut app App) handle_register(mut ctx Context, username string, email string, password string, no_redirect string) veb.Result {
215 user_count := app.get_users_count_with_reconnect() or {
216 eprintln('[register] get_users_count failed: ${err}')
217 return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}')
218 }
219 no_users := user_count == 0
220 println('USERNAME=${username}')
221
222 if username in ['login', 'register', 'new', 'new_post', 'oauth'] {
223 return app.register_failed(mut ctx, no_redirect, 'Username `${username}` is not available')
224 }
225
226 user_chars := username.bytes()
227
228 if user_chars.len > max_username_len {
229 return app.register_failed(mut ctx, no_redirect,
230 'Username is too long (max. ${max_username_len})')
231 }
232
233 if username.contains('--') {
234 return app.register_failed(mut ctx, no_redirect, 'Username cannot contain two hyphens')
235 }
236
237 if user_chars[0] == `-` || user_chars.last() == `-` {
238 return app.register_failed(mut ctx, no_redirect,
239 'Username cannot begin or end with a hyphen')
240 }
241
242 for ch in user_chars {
243 if !ch.is_letter() && !ch.is_digit() && ch != `-` {
244 return app.register_failed(mut ctx, no_redirect,
245 'Username cannot contain special characters')
246 }
247 }
248
249 is_username_valid := validation.is_username_valid(username)
250
251 if !is_username_valid {
252 return app.register_failed(mut ctx, no_redirect, 'Username is not valid')
253 }
254
255 if password == '' {
256 return app.register_failed(mut ctx, no_redirect, 'Password cannot be empty')
257 }
258
259 salt := generate_salt()
260 hashed_password := hash_password_with_salt(password, salt)
261
262 if username == '' || email == '' {
263 return app.register_failed(mut ctx, no_redirect, 'Username or Email cannot be emtpy')
264 }
265
266 // TODO: refactor
267 is_registered := app.register_user(username, hashed_password, salt, [email], false, no_users) or {
268 eprintln('[register] register_user failed for username=${username} email=${email}: ${err}')
269 msg := if is_unique_constraint_error(err) {
270 'Username `${username}` or email `${email}` is already in use'
271 } else {
272 'Failed to register: ${err.msg()}'
273 }
274 return app.register_failed(mut ctx, no_redirect, msg)
275 }
276
277 if !is_registered {
278 eprintln('[register] register_user returned false for username=${username} email=${email} (user already exists or insertion mismatch — see prior info logs)')
279 return app.register_failed(mut ctx, no_redirect,
280 'Failed to register: user already exists or could not be inserted')
281 }
282
283 user := app.get_user_by_username(username) or {
284 return app.register_failed(mut ctx, no_redirect, 'User already exists')
285 }
286
287 if no_users {
288 app.add_admin(user.id) or { app.info(err.str()) }
289 }
290
291 client_ip := 'ip' // ctx.ip() // XTODO
292
293 app.auth_user(mut ctx, user, client_ip) or {
294 eprintln('[register] auth_user failed for username=${username}: ${err}')
295 return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}')
296 }
297 app.add_security_log(user_id: user.id, kind: .registered) or { app.info(err.str()) }
298
299 if no_redirect == '1' {
300 return ctx.text('ok')
301 }
302
303 return ctx.redirect('/' + username)
304}
305
306@['/api/v1/users/avatar'; post]
307pub fn (mut app App) handle_upload_avatar(mut ctx Context) veb.Result {
308 if !ctx.logged_in {
309 return ctx.not_found()
310 }
311
312 avatar := ctx.files['file'].first()
313 file_content_type := avatar.content_type
314 file_content := avatar.data
315
316 file_extension := extract_file_extension_from_mime_type(file_content_type) or {
317 response := api.ApiErrorResponse{
318 message: err.str()
319 }
320
321 return ctx.json(response)
322 }
323
324 is_content_size_valid := validate_avatar_file_size(file_content)
325
326 if !is_content_size_valid {
327 response := api.ApiErrorResponse{
328 message: 'This file is too large to be uploaded'
329 }
330
331 return ctx.json(response)
332 }
333
334 username := ctx.user.username
335 avatar_filename := '${username}.${file_extension}'
336
337 app.write_user_avatar(avatar_filename, file_content)
338 app.update_user_avatar(ctx.user.id, avatar_filename) or {
339 response := api.ApiErrorResponse{
340 message: 'There was an error while updating the avatar'
341 }
342
343 return ctx.json(response)
344 }
345
346 avatar_file_path := app.build_avatar_file_path(avatar_filename)
347 avatar_file_url := app.build_avatar_file_url(avatar_filename)
348
349 app.serve_static(avatar_file_url, avatar_file_path) or { panic(err) }
350
351 response := api.ApiResponse{
352 success: true
353 }
354
355 return ctx.json(response)
356}
357