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/login'; post] 75fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result { 76 user := app.get_user_by_name(username) or { 77 ctx.error('invalid credentials') 78 return ctx.redirect('/login') 79 } 80 81 if !auth.compare_password_with_hash(password, user.password_salt, user.password) { 82 ctx.error('invalid credentials') 83 return ctx.redirect('/login') 84 } 85 86 token := app.auth.add_token(user.id, ctx.ip()) or { 87 eprintln('failed to add token on log in: ${err}') 88 ctx.error('could not create token for user with id ${user.id}') 89 return ctx.redirect('/login') 90 } 91 92 ctx.set_cookie( 93 name: 'token' 94 value: token 95 same_site: .same_site_none_mode 96 secure: true 97 path: '/' 98 ) 99 100 return ctx.redirect('/') 101} 102 103@['/api/user/logout'] 104fn (mut app App) api_user_logout(mut ctx Context) veb.Result { 105 if token := ctx.get_cookie('token') { 106 if user := app.get_user_by_token(ctx, token) { 107 app.auth.delete_tokens_for_ip(ctx.ip()) or { 108 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()}') 109 return ctx.redirect('/login') 110 } 111 } else { 112 eprintln('failed to get user for token for logout') 113 } 114 } else { 115 eprintln('failed to get token cookie for logout') 116 } 117 118 ctx.set_cookie( 119 name: 'token' 120 value: '' 121 same_site: .same_site_none_mode 122 secure: true 123 path: '/' 124 ) 125 126 return ctx.redirect('/login') 127} 128 129@['/api/user/full_logout'] 130fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result { 131 if token := ctx.get_cookie('token') { 132 if user := app.get_user_by_token(ctx, token) { 133 app.auth.delete_tokens_for_user(user.id) or { 134 eprintln('failed to yeet tokens for ${user.id}') 135 return ctx.redirect('/login') 136 } 137 } else { 138 eprintln('failed to get user for token for full_logout') 139 } 140 } else { 141 eprintln('failed to get token cookie for full_logout') 142 } 143 144 ctx.set_cookie( 145 name: 'token' 146 value: '' 147 same_site: .same_site_none_mode 148 secure: true 149 path: '/' 150 ) 151 152 return ctx.redirect('/login') 153} 154 155@['/api/user/set_nickname'; post] 156fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result { 157 user := app.whoami(mut ctx) or { 158 ctx.error('you are not logged in!') 159 return ctx.redirect('/login') 160 } 161 162 mut clean_nickname := ?string(nickname.trim_space()) 163 if clean_nickname or { '' } == '' { 164 clean_nickname = none 165 } 166 167 // validate 168 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) { 169 ctx.error('invalid nickname') 170 return ctx.redirect('/me') 171 } 172 173 sql app.db { 174 update User set nickname = clean_nickname where id == user.id 175 } or { 176 ctx.error('failed to change nickname') 177 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})') 178 return ctx.redirect('/me') 179 } 180 181 return ctx.redirect('/me') 182} 183 184@['/api/user/set_muted'; post] 185fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result { 186 user := app.whoami(mut ctx) or { 187 ctx.error('you are not logged in!') 188 return ctx.redirect('/login') 189 } 190 191 if user.admin || app.config.dev_mode { 192 sql app.db { 193 update User set muted = muted where id == user.id 194 } or { 195 ctx.error('failed to change mute status') 196 eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})') 197 return ctx.redirect('/user/${user.username}') 198 } 199 return ctx.redirect('/user/${user.username}') 200 } else { 201 ctx.error('insufficient permissions!') 202 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})') 203 return ctx.redirect('/user/${user.username}') 204 } 205} 206 207@['/api/user/set_theme'; post] 208fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result { 209 if !app.config.instance.allow_changing_theme { 210 ctx.error('this instance disallows changing themes :(') 211 return ctx.redirect('/me') 212 } 213 214 user := app.whoami(mut ctx) or { 215 ctx.error('you are not logged in!') 216 return ctx.redirect('/login') 217 } 218 219 mut theme := ?string(none) 220 if url.trim_space() != '' { 221 theme = url.trim_space() 222 } 223 224 sql app.db { 225 update User set theme = theme where id == user.id 226 } or { 227 ctx.error('failed to change theme') 228 eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})') 229 return ctx.redirect('/me') 230 } 231 232 return ctx.redirect('/me') 233} 234 235@['/api/user/set_pronouns'; post] 236fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result { 237 user := app.whoami(mut ctx) or { 238 ctx.error('you are not logged in!') 239 return ctx.redirect('/login') 240 } 241 242 clean_pronouns := pronouns.trim_space() 243 if !app.validators.pronouns.validate(clean_pronouns) { 244 ctx.error('invalid pronouns') 245 return ctx.redirect('/me') 246 } 247 248 sql app.db { 249 update User set pronouns = clean_pronouns where id == user.id 250 } or { 251 ctx.error('failed to change pronouns') 252 eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})') 253 return ctx.redirect('/me') 254 } 255 256 return ctx.redirect('/me') 257} 258 259@['/api/user/set_bio'; post] 260fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result { 261 user := app.whoami(mut ctx) or { 262 ctx.error('you are not logged in!') 263 return ctx.redirect('/login') 264 } 265 266 clean_bio := bio.trim_space() 267 if !app.validators.user_bio.validate(clean_bio) { 268 ctx.error('invalid bio') 269 return ctx.redirect('/me') 270 } 271 272 sql app.db { 273 update User set bio = clean_bio where id == user.id 274 } or { 275 ctx.error('failed to change bio') 276 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})') 277 return ctx.redirect('/me') 278 } 279 280 return ctx.redirect('/me') 281} 282 283@['/api/user/get_name'] 284fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result { 285 user := app.get_user_by_name(username) or { return ctx.server_error('no such user') } 286 return ctx.text(user.get_name()) 287} 288 289/// user/notification /// 290 291@['/api/user/notification/clear'] 292fn (mut app App) api_user_notification_clear(mut ctx Context, id int) veb.Result { 293 if !ctx.is_logged_in() { 294 ctx.error('you are not logged in!') 295 return ctx.redirect('/login') 296 } 297 sql app.db { 298 delete from Notification where id == id 299 } or { 300 ctx.error('failed to delete notification') 301 return ctx.redirect('/inbox') 302 } 303 return ctx.redirect('/inbox') 304} 305 306@['/api/user/notification/clear_all'] 307fn (mut app App) api_user_notification_clear_all(mut ctx Context) veb.Result { 308 user := app.whoami(mut ctx) or { 309 ctx.error('you are not logged in!') 310 return ctx.redirect('/login') 311 } 312 sql app.db { 313 delete from Notification where user_id == user.id 314 } or { 315 ctx.error('failed to delete notifications') 316 return ctx.redirect('/inbox') 317 } 318 return ctx.redirect('/inbox') 319} 320 321@['/api/user/delete'] 322fn (mut app App) api_user_delete(mut ctx Context, id int) veb.Result { 323 user := app.whoami(mut ctx) or { 324 ctx.error('you are not logged in!') 325 return ctx.redirect('/login') 326 } 327 328 println('attempting to delete ${id} as ${user.id}') 329 330 if user.admin || user.id == id { 331 // yeet 332 sql app.db { 333 delete from User where id == id 334 delete from Like where user_id == id 335 delete from Notification where user_id == id 336 } or { 337 ctx.error('failed to delete user: ${id}') 338 return ctx.redirect('/') 339 } 340 341 // delete posts and their likes 342 posts_from_this_user := sql app.db { 343 select from Post where author_id == id 344 } or { [] } 345 346 for post in posts_from_this_user { 347 sql app.db { 348 delete from Like where post_id == post.id 349 delete from LikeCache where post_id == post.id 350 } or { 351 eprintln('failed to delete like cache for post during user deletion: ${post.id}') 352 } 353 } 354 355 sql app.db { 356 delete from Post where author_id == id 357 } or { 358 eprintln('failed to delete posts by deleting user: ${user.id}') 359 } 360 361 app.auth.delete_tokens_for_user(id) or { 362 eprintln('failed to delete tokens for user during deletion: ${id}') 363 } 364 // log out 365 if user.id == id { 366 ctx.set_cookie( 367 name: 'token' 368 value: '' 369 same_site: .same_site_none_mode 370 secure: true 371 path: '/' 372 ) 373 } 374 println('deleted user ${id}') 375 } else { 376 ctx.error('be nice. deleting other users is off-limits.') 377 } 378 379 return ctx.redirect('/') 380} 381 382////// post ////// 383 384@['/api/post/new_post'; post] 385fn (mut app App) api_post_new_post(mut ctx Context, replying_to int, title string, body string) veb.Result { 386 user := app.whoami(mut ctx) or { 387 ctx.error('not logged in!') 388 return ctx.redirect('/login') 389 } 390 391 if user.muted { 392 ctx.error('you are muted!') 393 return ctx.redirect('/post/new') 394 } 395 396 // validate title 397 if !app.validators.post_title.validate(title) { 398 ctx.error('invalid title') 399 return ctx.redirect('/post/new') 400 } 401 402 // validate body 403 if !app.validators.post_body.validate(body) { 404 ctx.error('invalid body') 405 return ctx.redirect('/post/new') 406 } 407 408 mut post := Post{ 409 author_id: user.id 410 title: title 411 body: body 412 } 413 414 if replying_to != 0 { 415 // check if replying post exists 416 app.get_post_by_id(replying_to) or { 417 ctx.error('the post you are trying to reply to does not exist') 418 return ctx.redirect('/post/new') 419 } 420 post.replying_to = replying_to 421 } 422 423 sql app.db { 424 insert post into Post 425 } or { 426 ctx.error('failed to post!') 427 println('failed to post: ${post} from user ${user.id}') 428 return ctx.redirect('/post/new') 429 } 430 431 // find the post's id to process mentions with 432 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) { 433 app.process_post_mentions(x) 434 return ctx.redirect('/post/${x.id}') 435 } else { 436 ctx.error('failed to get_post_by_timestamp_and_author for ${post}') 437 return ctx.redirect('/me') 438 } 439} 440 441@['/api/post/delete'; post] 442fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result { 443 user := app.whoami(mut ctx) or { 444 ctx.error('not logged in!') 445 return ctx.redirect('/login') 446 } 447 448 post := app.get_post_by_id(id) or { 449 ctx.error('post does not exist') 450 return ctx.redirect('/') 451 } 452 453 if user.admin || user.id == post.author_id { 454 sql app.db { 455 delete from Post where id == id 456 delete from Like where post_id == id 457 } or { 458 ctx.error('failed to delete post') 459 eprintln('failed to delete post: ${id}') 460 return ctx.redirect('/') 461 } 462 println('deleted post: ${id}') 463 return ctx.redirect('/') 464 } else { 465 ctx.error('insufficient permissions!') 466 eprintln('insufficient perms to delete post: ${id} (${user.id})') 467 return ctx.redirect('/') 468 } 469} 470 471@['/api/post/like'] 472fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 473 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 474 475 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 476 477 if app.does_user_like_post(user.id, post.id) { 478 sql app.db { 479 delete from Like where user_id == user.id && post_id == post.id 480 // yeet the old cached like value 481 delete from LikeCache where post_id == post.id 482 } or { 483 eprintln('user ${user.id} failed to unlike post ${id}') 484 return ctx.server_error('failed to unlike post') 485 } 486 return ctx.ok('unliked post') 487 } else { 488 // remove the old dislike, if it exists 489 if app.does_user_dislike_post(user.id, post.id) { 490 sql app.db { 491 delete from Like where user_id == user.id && post_id == post.id 492 } or { 493 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it') 494 } 495 } 496 497 like := Like{ 498 user_id: user.id 499 post_id: post.id 500 is_like: true 501 } 502 sql app.db { 503 insert like into Like 504 // yeet the old cached like value 505 delete from LikeCache where post_id == post.id 506 } or { 507 eprintln('user ${user.id} failed to like post ${id}') 508 return ctx.server_error('failed to like post') 509 } 510 return ctx.ok('liked post') 511 } 512} 513 514@['/api/post/dislike'] 515fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 516 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 517 518 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 519 520 if app.does_user_dislike_post(user.id, post.id) { 521 sql app.db { 522 delete from Like where user_id == user.id && post_id == post.id 523 // yeet the old cached like value 524 delete from LikeCache where post_id == post.id 525 } or { 526 eprintln('user ${user.id} failed to unlike post ${id}') 527 return ctx.server_error('failed to unlike post') 528 } 529 return ctx.ok('undisliked post') 530 } else { 531 // remove the old like, if it exists 532 if app.does_user_like_post(user.id, post.id) { 533 sql app.db { 534 delete from Like where user_id == user.id && post_id == post.id 535 } or { 536 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it') 537 } 538 } 539 540 like := Like{ 541 user_id: user.id 542 post_id: post.id 543 is_like: false 544 } 545 sql app.db { 546 insert like into Like 547 // yeet the old cached like value 548 delete from LikeCache where post_id == post.id 549 } or { 550 eprintln('user ${user.id} failed to dislike post ${id}') 551 return ctx.server_error('failed to dislike post') 552 } 553 return ctx.ok('disliked post') 554 } 555} 556 557@['/api/post/get_title'] 558fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result { 559 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') } 560 return ctx.text(post.title) 561} 562 563@['/api/post/edit'; post] 564fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result { 565 user := app.whoami(mut ctx) or { 566 ctx.error('not logged in!') 567 return ctx.redirect('/login') 568 } 569 post := app.get_post_by_id(id) or { 570 ctx.error('no such post') 571 return ctx.redirect('/') 572 } 573 if post.author_id != user.id { 574 ctx.error('insufficient permissions') 575 return ctx.redirect('/') 576 } 577 578 sql app.db { 579 update Post set body = body, title = title where id == id 580 } or { 581 eprintln('failed to update post') 582 ctx.error('failed to update post') 583 return ctx.redirect('/') 584 } 585 586 return ctx.redirect('/post/${id}') 587} 588 589////// site ////// 590 591@['/api/site/set_motd'; post] 592fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result { 593 user := app.whoami(mut ctx) or { 594 ctx.error('not logged in!') 595 return ctx.redirect('/login') 596 } 597 598 if user.admin { 599 sql app.db { 600 update Site set motd = motd where id == 1 601 } or { 602 ctx.error('failed to set motd') 603 eprintln('failed to set motd: ${motd}') 604 return ctx.redirect('/') 605 } 606 println('set motd to: ${motd}') 607 return ctx.redirect('/') 608 } else { 609 ctx.error('insufficient permissions!') 610 eprintln('insufficient perms to set motd to: ${motd} (${user.id})') 611 return ctx.redirect('/') 612 } 613}