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