a mini social media app for small communities
1module main
2
3import veb
4import auth
5import entity { Like, LikeCache, Post, Site, User, Notification }
6
7////// user //////
8
9@['/api/user/register'; post]
10fn (mut app App) api_user_register(mut ctx Context, username string, password string) veb.Result {
11 if app.get_user_by_name(username) != none {
12 ctx.error('username taken')
13 return ctx.redirect('/register')
14 }
15
16 // validate username
17 if !app.validators.username.validate(username) {
18 ctx.error('invalid username')
19 return ctx.redirect('/register')
20 }
21
22 // validate password
23 if !app.validators.password.validate(password) {
24 ctx.error('invalid password')
25 return ctx.redirect('/register')
26 }
27
28 salt := auth.generate_salt()
29 mut user := User{
30 username: username
31 password: auth.hash_password_with_salt(password, salt)
32 password_salt: salt
33 }
34
35 if app.config.instance.default_theme != '' {
36 user.theme = app.config.instance.default_theme
37 }
38
39 sql app.db {
40 insert user into User
41 } or {
42 eprintln('failed to insert user ${user}')
43 return ctx.redirect('/')
44 }
45
46 println('reg: ${username}')
47
48 if x := app.get_user_by_name(username) {
49 app.send_notification_to(
50 x.id,
51 app.config.welcome.summary.replace('%s', x.get_name()),
52 app.config.welcome.body.replace('%s', x.get_name())
53 )
54 token := app.auth.add_token(x.id, ctx.ip()) or {
55 eprintln(err)
56 ctx.error('could not create token for user with id ${x.id}')
57 return ctx.redirect('/')
58 }
59 ctx.set_cookie(
60 name: 'token'
61 value: token
62 same_site: .same_site_none_mode
63 secure: true
64 path: '/'
65 )
66 } else {
67 eprintln('could not log into newly-created user: ${user}')
68 ctx.error('could not log into newly-created user.')
69 }
70
71 return ctx.redirect('/')
72}
73
74@['/api/user/set_username'; post]
75fn (mut app App) api_user_set_username(mut ctx Context, new_username string) veb.Result {
76 user := app.whoami(mut ctx) or {
77 ctx.error('you are not logged in!')
78 return ctx.redirect('/login')
79 }
80
81 if app.get_user_by_name(new_username) != none {
82 ctx.error('username taken')
83 return ctx.redirect('/settings')
84 }
85
86 // validate username
87 if !app.validators.username.validate(new_username) {
88 ctx.error('invalid username')
89 return ctx.redirect('/settings')
90 }
91
92 sql app.db {
93 update User set username = new_username where id == user.id
94 } or {
95 eprintln('failed to update username for ${user.id}')
96 ctx.error('failed to update username')
97 }
98
99 return ctx.redirect('/settings')
100}
101
102@['/api/user/set_password'; post]
103fn (mut app App) api_user_set_password(mut ctx Context, current_password string, new_password string) veb.Result {
104 user := app.whoami(mut ctx) or {
105 ctx.error('you are not logged in!')
106 return ctx.redirect('/login')
107 }
108
109 if !auth.compare_password_with_hash(current_password, user.password_salt, user.password) {
110 ctx.error('current_password is incorrect')
111 return ctx.redirect('/settings')
112 }
113
114 // validate password
115 if !app.validators.password.validate(new_password) {
116 ctx.error('invalid password')
117 return ctx.redirect('/settings')
118 }
119
120 // invalidate tokens
121 app.auth.delete_tokens_for_user(user.id) or {
122 eprintln('failed to yeet tokens during password deletion for ${user.id} (${err})')
123 return ctx.redirect('/settings')
124 }
125
126 hashed_new_password := auth.hash_password_with_salt(new_password, user.password_salt)
127
128 sql app.db {
129 update User set password = hashed_new_password where id == user.id
130 } or {
131 eprintln('failed to update password for ${user.id}')
132 ctx.error('failed to update password')
133 }
134
135 return ctx.redirect('/login')
136}
137
138@['/api/user/login'; post]
139fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result {
140 user := app.get_user_by_name(username) or {
141 ctx.error('invalid credentials')
142 return ctx.redirect('/login')
143 }
144
145 if !auth.compare_password_with_hash(password, user.password_salt, user.password) {
146 ctx.error('invalid credentials')
147 return ctx.redirect('/login')
148 }
149
150 token := app.auth.add_token(user.id, ctx.ip()) or {
151 eprintln('failed to add token on log in: ${err}')
152 ctx.error('could not create token for user with id ${user.id}')
153 return ctx.redirect('/login')
154 }
155
156 ctx.set_cookie(
157 name: 'token'
158 value: token
159 same_site: .same_site_none_mode
160 secure: true
161 path: '/'
162 )
163
164 return ctx.redirect('/')
165}
166
167@['/api/user/logout']
168fn (mut app App) api_user_logout(mut ctx Context) veb.Result {
169 if token := ctx.get_cookie('token') {
170 if user := app.get_user_by_token(ctx, token) {
171 app.auth.delete_tokens_for_ip(ctx.ip()) or {
172 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()} (${err})')
173 return ctx.redirect('/login')
174 }
175 } else {
176 eprintln('failed to get user for token for logout')
177 }
178 } else {
179 eprintln('failed to get token cookie for logout')
180 }
181
182 ctx.set_cookie(
183 name: 'token'
184 value: ''
185 same_site: .same_site_none_mode
186 secure: true
187 path: '/'
188 )
189
190 return ctx.redirect('/login')
191}
192
193@['/api/user/full_logout']
194fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result {
195 if token := ctx.get_cookie('token') {
196 if user := app.get_user_by_token(ctx, token) {
197 app.auth.delete_tokens_for_user(user.id) or {
198 eprintln('failed to yeet tokens for ${user.id}')
199 return ctx.redirect('/login')
200 }
201 } else {
202 eprintln('failed to get user for token for full_logout')
203 }
204 } else {
205 eprintln('failed to get token cookie for full_logout')
206 }
207
208 ctx.set_cookie(
209 name: 'token'
210 value: ''
211 same_site: .same_site_none_mode
212 secure: true
213 path: '/'
214 )
215
216 return ctx.redirect('/login')
217}
218
219@['/api/user/set_nickname'; post]
220fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result {
221 user := app.whoami(mut ctx) or {
222 ctx.error('you are not logged in!')
223 return ctx.redirect('/login')
224 }
225
226 mut clean_nickname := ?string(nickname.trim_space())
227 if clean_nickname or { '' } == '' {
228 clean_nickname = none
229 }
230
231 // validate
232 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) {
233 ctx.error('invalid nickname')
234 return ctx.redirect('/me')
235 }
236
237 sql app.db {
238 update User set nickname = clean_nickname where id == user.id
239 } or {
240 ctx.error('failed to change nickname')
241 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})')
242 return ctx.redirect('/me')
243 }
244
245 return ctx.redirect('/me')
246}
247
248@['/api/user/set_muted'; post]
249fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result {
250 user := app.whoami(mut ctx) or {
251 ctx.error('you are not logged in!')
252 return ctx.redirect('/login')
253 }
254
255 if user.admin || app.config.dev_mode {
256 sql app.db {
257 update User set muted = muted where id == user.id
258 } or {
259 ctx.error('failed to change mute status')
260 eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})')
261 return ctx.redirect('/user/${user.username}')
262 }
263 return ctx.redirect('/user/${user.username}')
264 } else {
265 ctx.error('insufficient permissions!')
266 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})')
267 return ctx.redirect('/user/${user.username}')
268 }
269}
270
271@['/api/user/set_theme'; post]
272fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result {
273 if !app.config.instance.allow_changing_theme {
274 ctx.error('this instance disallows changing themes :(')
275 return ctx.redirect('/me')
276 }
277
278 user := app.whoami(mut ctx) or {
279 ctx.error('you are not logged in!')
280 return ctx.redirect('/login')
281 }
282
283 mut theme := ?string(none)
284 if url.trim_space() != '' {
285 theme = url.trim_space()
286 }
287
288 sql app.db {
289 update User set theme = theme where id == user.id
290 } or {
291 ctx.error('failed to change theme')
292 eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})')
293 return ctx.redirect('/me')
294 }
295
296 return ctx.redirect('/me')
297}
298
299@['/api/user/set_pronouns'; post]
300fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result {
301 user := app.whoami(mut ctx) or {
302 ctx.error('you are not logged in!')
303 return ctx.redirect('/login')
304 }
305
306 clean_pronouns := pronouns.trim_space()
307 if !app.validators.pronouns.validate(clean_pronouns) {
308 ctx.error('invalid pronouns')
309 return ctx.redirect('/me')
310 }
311
312 sql app.db {
313 update User set pronouns = clean_pronouns where id == user.id
314 } or {
315 ctx.error('failed to change pronouns')
316 eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})')
317 return ctx.redirect('/me')
318 }
319
320 return ctx.redirect('/me')
321}
322
323@['/api/user/set_bio'; post]
324fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result {
325 user := app.whoami(mut ctx) or {
326 ctx.error('you are not logged in!')
327 return ctx.redirect('/login')
328 }
329
330 clean_bio := bio.trim_space()
331 if !app.validators.user_bio.validate(clean_bio) {
332 ctx.error('invalid bio')
333 return ctx.redirect('/me')
334 }
335
336 sql app.db {
337 update User set bio = clean_bio where id == user.id
338 } or {
339 ctx.error('failed to change bio')
340 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})')
341 return ctx.redirect('/me')
342 }
343
344 return ctx.redirect('/me')
345}
346
347@['/api/user/get_name']
348fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result {
349 user := app.get_user_by_name(username) or { return ctx.server_error('no such user') }
350 return ctx.text(user.get_name())
351}
352
353/// user/notification ///
354
355@['/api/user/notification/clear']
356fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result {
357 if !ctx.is_logged_in() {
358 ctx.error('you are not logged in!')
359 return ctx.redirect('/login')
360 }
361 sql app.db {
362 delete from Notification where id == id
363 } or {
364 ctx.error('failed to delete notification')
365 return ctx.redirect('/inbox')
366 }
367 return ctx.redirect('/inbox')
368}
369
370@['/api/user/notification/clear_all']
371fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result {
372 user := app.whoami(mut ctx) or {
373 ctx.error('you are not logged in!')
374 return ctx.redirect('/login')
375 }
376 sql app.db {
377 delete from Notification where user_id == user.id
378 } or {
379 ctx.error('failed to delete notifications')
380 return ctx.redirect('/inbox')
381 }
382 return ctx.redirect('/inbox')
383}
384
385@['/api/user/delete']
386fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result {
387 user := app.whoami(mut ctx) or {
388 ctx.error('you are not logged in!')
389 return ctx.redirect('/login')
390 }
391
392 println('attempting to delete ${id} as ${user.id}')
393
394 if user.admin || user.id == id {
395 // yeet
396 sql app.db {
397 delete from User where id == id
398 delete from Like where user_id == id
399 delete from Notification where user_id == id
400 } or {
401 ctx.error('failed to delete user: ${id}')
402 return ctx.redirect('/')
403 }
404
405 // delete posts and their likes
406 posts_from_this_user := sql app.db {
407 select from Post where author_id == id
408 } or { [] }
409
410 for post in posts_from_this_user {
411 sql app.db {
412 delete from Like where post_id == post.id
413 delete from LikeCache where post_id == post.id
414 } or {
415 eprintln('failed to delete like cache for post during user deletion: ${post.id}')
416 }
417 }
418
419 sql app.db {
420 delete from Post where author_id == id
421 } or {
422 eprintln('failed to delete posts by deleting user: ${user.id}')
423 }
424
425 app.auth.delete_tokens_for_user(id) or {
426 eprintln('failed to delete tokens for user during deletion: ${id}')
427 }
428 // log out
429 if user.id == id {
430 ctx.set_cookie(
431 name: 'token'
432 value: ''
433 same_site: .same_site_none_mode
434 secure: true
435 path: '/'
436 )
437 }
438 println('deleted user ${id}')
439 } else {
440 ctx.error('be nice. deleting other users is off-limits.')
441 }
442
443 return ctx.redirect('/')
444}
445
446////// post //////
447
448@['/api/post/new_post'; post]
449fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result {
450 user := app.whoami(mut ctx) or {
451 ctx.error('not logged in!')
452 return ctx.redirect('/login')
453 }
454
455 if user.muted {
456 ctx.error('you are muted!')
457 return ctx.redirect('/post/new')
458 }
459
460 // validate title
461 if !app.validators.post_title.validate(title) {
462 ctx.error('invalid title')
463 return ctx.redirect('/post/new')
464 }
465
466 // validate body
467 if !app.validators.post_body.validate(body) {
468 ctx.error('invalid body')
469 return ctx.redirect('/post/new')
470 }
471
472 mut post := Post{
473 author_id: user.id
474 title: title
475 body: body
476 }
477
478 if replying_to != 0 {
479 // check if replying post exists
480 app.get_post_by_id(replying_to) or {
481 ctx.error('the post you are trying to reply to does not exist')
482 return ctx.redirect('/post/new')
483 }
484 post.replying_to = replying_to
485 }
486
487 sql app.db {
488 insert post into Post
489 } or {
490 ctx.error('failed to post!')
491 println('failed to post: ${post} from user ${user.id}')
492 return ctx.redirect('/post/new')
493 }
494
495 // find the post's id to process mentions with
496 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) {
497 app.process_post_mentions(x)
498 return ctx.redirect('/post/${x.id}')
499 } else {
500 ctx.error('failed to get_post_by_timestamp_and_author for ${post}')
501 return ctx.redirect('/me')
502 }
503}
504
505@['/api/post/delete'; post]
506fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result {
507 user := app.whoami(mut ctx) or {
508 ctx.error('not logged in!')
509 return ctx.redirect('/login')
510 }
511
512 post := app.get_post_by_id(id) or {
513 ctx.error('post does not exist')
514 return ctx.redirect('/')
515 }
516
517 if user.admin || user.id == post.author_id {
518 sql app.db {
519 delete from Post where id == id
520 delete from Like where post_id == id
521 } or {
522 ctx.error('failed to delete post')
523 eprintln('failed to delete post: ${id}')
524 return ctx.redirect('/')
525 }
526 println('deleted post: ${id}')
527 return ctx.redirect('/')
528 } else {
529 ctx.error('insufficient permissions!')
530 eprintln('insufficient perms to delete post: ${id} (${user.id})')
531 return ctx.redirect('/')
532 }
533}
534
535@['/api/post/like']
536fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result {
537 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
538
539 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
540
541 if app.does_user_like_post(user.id, post.id) {
542 sql app.db {
543 delete from Like where user_id == user.id && post_id == post.id
544 // yeet the old cached like value
545 delete from LikeCache where post_id == post.id
546 } or {
547 eprintln('user ${user.id} failed to unlike post ${id}')
548 return ctx.server_error('failed to unlike post')
549 }
550 return ctx.ok('unliked post')
551 } else {
552 // remove the old dislike, if it exists
553 if app.does_user_dislike_post(user.id, post.id) {
554 sql app.db {
555 delete from Like where user_id == user.id && post_id == post.id
556 } or {
557 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it')
558 }
559 }
560
561 like := Like{
562 user_id: user.id
563 post_id: post.id
564 is_like: true
565 }
566 sql app.db {
567 insert like into Like
568 // yeet the old cached like value
569 delete from LikeCache where post_id == post.id
570 } or {
571 eprintln('user ${user.id} failed to like post ${id}')
572 return ctx.server_error('failed to like post')
573 }
574 return ctx.ok('liked post')
575 }
576}
577
578@['/api/post/dislike']
579fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result {
580 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') }
581
582 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') }
583
584 if app.does_user_dislike_post(user.id, post.id) {
585 sql app.db {
586 delete from Like where user_id == user.id && post_id == post.id
587 // yeet the old cached like value
588 delete from LikeCache where post_id == post.id
589 } or {
590 eprintln('user ${user.id} failed to unlike post ${id}')
591 return ctx.server_error('failed to unlike post')
592 }
593 return ctx.ok('undisliked post')
594 } else {
595 // remove the old like, if it exists
596 if app.does_user_like_post(user.id, post.id) {
597 sql app.db {
598 delete from Like where user_id == user.id && post_id == post.id
599 } or {
600 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it')
601 }
602 }
603
604 like := Like{
605 user_id: user.id
606 post_id: post.id
607 is_like: false
608 }
609 sql app.db {
610 insert like into Like
611 // yeet the old cached like value
612 delete from LikeCache where post_id == post.id
613 } or {
614 eprintln('user ${user.id} failed to dislike post ${id}')
615 return ctx.server_error('failed to dislike post')
616 }
617 return ctx.ok('disliked post')
618 }
619}
620
621@['/api/post/get_title']
622fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result {
623 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') }
624 return ctx.text(post.title)
625}
626
627@['/api/post/edit'; post]
628fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result {
629 user := app.whoami(mut ctx) or {
630 ctx.error('not logged in!')
631 return ctx.redirect('/login')
632 }
633 post := app.get_post_by_id(id) or {
634 ctx.error('no such post')
635 return ctx.redirect('/')
636 }
637 if post.author_id != user.id {
638 ctx.error('insufficient permissions')
639 return ctx.redirect('/')
640 }
641
642 sql app.db {
643 update Post set body = body, title = title where id == id
644 } or {
645 eprintln('failed to update post')
646 ctx.error('failed to update post')
647 return ctx.redirect('/')
648 }
649
650 return ctx.redirect('/post/${id}')
651}
652
653@['/api/post/pin'; post]
654fn (mut app App) api_post_pin(mut ctx Context, id int) veb.Result {
655 user := app.whoami(mut ctx) or {
656 ctx.error('not logged in!')
657 return ctx.redirect('/login')
658 }
659
660 if user.admin {
661 sql app.db {
662 update Post set pinned = true where id == id
663 } or {
664 eprintln('failed to pin post: ${id}')
665 ctx.error('failed to pin post')
666 return ctx.redirect('/post/${id}')
667 }
668 return ctx.redirect('/post/${id}')
669 } else {
670 ctx.error('insufficient permissions!')
671 eprintln('insufficient perms to pin post: ${id} (${user.id})')
672 return ctx.redirect('/')
673 }
674}
675
676////// site //////
677
678@['/api/site/set_motd'; post]
679fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result {
680 user := app.whoami(mut ctx) or {
681 ctx.error('not logged in!')
682 return ctx.redirect('/login')
683 }
684
685 if user.admin {
686 sql app.db {
687 update Site set motd = motd where id == 1
688 } or {
689 ctx.error('failed to set motd')
690 eprintln('failed to set motd: ${motd}')
691 return ctx.redirect('/')
692 }
693 println('set motd to: ${motd}')
694 return ctx.redirect('/')
695 } else {
696 ctx.error('insufficient permissions!')
697 eprintln('insufficient perms to set motd to: ${motd} (${user.id})')
698 return ctx.redirect('/')
699 }
700}