ggdgsdbsdbbb / user / user_routes.v
353 lines · 283 sloc · 10.21 KB · 53a36b4a41aa6dcaa188b940342a2af42a3a96e9
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 user_count := app.get_users_count() or { 0 }
194 no_users := user_count == 0
195
196 ctx.current_path = ''
197
198 return $veb.html()
199}
200
201fn (mut app App) register_failed(mut ctx Context, no_redirect string, msg string) veb.Result {
202 if no_redirect == '1' {
203 ctx.res.set_status(.bad_request)
204 return ctx.text(msg)
205 }
206 ctx.error(msg)
207 return app.register(mut ctx)
208}
209
210@['/register'; post]
211pub fn (mut app App) handle_register(mut ctx Context, username string, email string, password string, no_redirect string) veb.Result {
212 user_count := app.get_users_count() or {
213 eprintln('[register] get_users_count failed: ${err}')
214 return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}')
215 }
216 no_users := user_count == 0
217 println('USERNAME=${username}')
218
219 if username in ['login', 'register', 'new', 'new_post', 'oauth'] {
220 return app.register_failed(mut ctx, no_redirect, 'Username `${username}` is not available')
221 }
222
223 user_chars := username.bytes()
224
225 if user_chars.len > max_username_len {
226 return app.register_failed(mut ctx, no_redirect,
227 'Username is too long (max. ${max_username_len})')
228 }
229
230 if username.contains('--') {
231 return app.register_failed(mut ctx, no_redirect, 'Username cannot contain two hyphens')
232 }
233
234 if user_chars[0] == `-` || user_chars.last() == `-` {
235 return app.register_failed(mut ctx, no_redirect,
236 'Username cannot begin or end with a hyphen')
237 }
238
239 for ch in user_chars {
240 if !ch.is_letter() && !ch.is_digit() && ch != `-` {
241 return app.register_failed(mut ctx, no_redirect,
242 'Username cannot contain special characters')
243 }
244 }
245
246 is_username_valid := validation.is_username_valid(username)
247
248 if !is_username_valid {
249 return app.register_failed(mut ctx, no_redirect, 'Username is not valid')
250 }
251
252 if password == '' {
253 return app.register_failed(mut ctx, no_redirect, 'Password cannot be empty')
254 }
255
256 salt := generate_salt()
257 hashed_password := hash_password_with_salt(password, salt)
258
259 if username == '' || email == '' {
260 return app.register_failed(mut ctx, no_redirect, 'Username or Email cannot be emtpy')
261 }
262
263 // TODO: refactor
264 is_registered := app.register_user(username, hashed_password, salt, [email], false, no_users) or {
265 eprintln('[register] register_user failed for username=${username} email=${email}: ${err}')
266 msg := if is_unique_constraint_error(err) {
267 'Username `${username}` or email `${email}` is already in use'
268 } else {
269 'Failed to register: ${err.msg()}'
270 }
271 return app.register_failed(mut ctx, no_redirect, msg)
272 }
273
274 if !is_registered {
275 eprintln('[register] register_user returned false for username=${username} email=${email} (user already exists or insertion mismatch — see prior info logs)')
276 return app.register_failed(mut ctx, no_redirect,
277 'Failed to register: user already exists or could not be inserted')
278 }
279
280 user := app.get_user_by_username(username) or {
281 return app.register_failed(mut ctx, no_redirect, 'User already exists')
282 }
283
284 if no_users {
285 app.add_admin(user.id) or { app.info(err.str()) }
286 }
287
288 client_ip := 'ip' // ctx.ip() // XTODO
289
290 app.auth_user(mut ctx, user, client_ip) or {
291 eprintln('[register] auth_user failed for username=${username}: ${err}')
292 return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}')
293 }
294 app.add_security_log(user_id: user.id, kind: .registered) or { app.info(err.str()) }
295
296 if no_redirect == '1' {
297 return ctx.text('ok')
298 }
299
300 return ctx.redirect('/' + username)
301}
302
303@['/api/v1/users/avatar'; post]
304pub fn (mut app App) handle_upload_avatar(mut ctx Context) veb.Result {
305 if !ctx.logged_in {
306 return ctx.not_found()
307 }
308
309 avatar := ctx.files['file'].first()
310 file_content_type := avatar.content_type
311 file_content := avatar.data
312
313 file_extension := extract_file_extension_from_mime_type(file_content_type) or {
314 response := api.ApiErrorResponse{
315 message: err.str()
316 }
317
318 return ctx.json(response)
319 }
320
321 is_content_size_valid := validate_avatar_file_size(file_content)
322
323 if !is_content_size_valid {
324 response := api.ApiErrorResponse{
325 message: 'This file is too large to be uploaded'
326 }
327
328 return ctx.json(response)
329 }
330
331 username := ctx.user.username
332 avatar_filename := '${username}.${file_extension}'
333
334 app.write_user_avatar(avatar_filename, file_content)
335 app.update_user_avatar(ctx.user.id, avatar_filename) or {
336 response := api.ApiErrorResponse{
337 message: 'There was an error while updating the avatar'
338 }
339
340 return ctx.json(response)
341 }
342
343 avatar_file_path := app.build_avatar_file_path(avatar_filename)
344 avatar_file_url := app.build_avatar_file_url(avatar_filename)
345
346 app.serve_static(avatar_file_url, avatar_file_path) or { panic(err) }
347
348 response := api.ApiResponse{
349 success: true
350 }
351
352 return ctx.json(response)
353}
354