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}