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, title string, body string) veb.Result { 386 user := app.whoami(mut ctx) or { 387 ctx.error('not logged in!') 388 return ctx.redirect('/') 389 } 390 391 if user.muted { 392 ctx.error('you are muted!') 393 return ctx.redirect('/me') 394 } 395 396 // validate title 397 if !app.validators.post_title.validate(title) { 398 ctx.error('invalid title') 399 return ctx.redirect('/me') 400 } 401 402 // validate body 403 if !app.validators.post_body.validate(body) { 404 ctx.error('invalid body') 405 return ctx.redirect('/me') 406 } 407 408 post := Post{ 409 author_id: user.id 410 title: title 411 body: body 412 } 413 414 sql app.db { 415 insert post into Post 416 } or { 417 ctx.error('failed to post!') 418 println('failed to post: ${post} from user ${user.id}') 419 return ctx.redirect('/me') 420 } 421 422 // find the post's id to process mentions with 423 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) { 424 app.process_post_mentions(x) 425 } else { 426 ctx.error('failed to get_post_by_timestamp_and_author for ${post}') 427 } 428 429 return ctx.redirect('/me') 430} 431 432@['/api/post/delete'; post] 433fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result { 434 user := app.whoami(mut ctx) or { 435 ctx.error('not logged in!') 436 return ctx.redirect('/login') 437 } 438 439 post := app.get_post_by_id(id) or { 440 ctx.error('post does not exist') 441 return ctx.redirect('/') 442 } 443 444 if user.admin || user.id == post.author_id { 445 sql app.db { 446 delete from Post where id == id 447 delete from Like where post_id == id 448 } or { 449 ctx.error('failed to delete post') 450 eprintln('failed to delete post: ${id}') 451 return ctx.redirect('/') 452 } 453 println('deleted post: ${id}') 454 return ctx.redirect('/') 455 } else { 456 ctx.error('insufficient permissions!') 457 eprintln('insufficient perms to delete post: ${id} (${user.id})') 458 return ctx.redirect('/') 459 } 460} 461 462@['/api/post/like'] 463fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 464 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 465 466 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 467 468 if app.does_user_like_post(user.id, post.id) { 469 sql app.db { 470 delete from Like where user_id == user.id && post_id == post.id 471 // yeet the old cached like value 472 delete from LikeCache where post_id == post.id 473 } or { 474 eprintln('user ${user.id} failed to unlike post ${id}') 475 return ctx.server_error('failed to unlike post') 476 } 477 return ctx.ok('unliked post') 478 } else { 479 // remove the old dislike, if it exists 480 if app.does_user_dislike_post(user.id, post.id) { 481 sql app.db { 482 delete from Like where user_id == user.id && post_id == post.id 483 } or { 484 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it') 485 } 486 } 487 488 like := Like{ 489 user_id: user.id 490 post_id: post.id 491 is_like: true 492 } 493 sql app.db { 494 insert like into Like 495 // yeet the old cached like value 496 delete from LikeCache where post_id == post.id 497 } or { 498 eprintln('user ${user.id} failed to like post ${id}') 499 return ctx.server_error('failed to like post') 500 } 501 return ctx.ok('liked post') 502 } 503} 504 505@['/api/post/dislike'] 506fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 507 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 508 509 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 510 511 if app.does_user_dislike_post(user.id, post.id) { 512 sql app.db { 513 delete from Like where user_id == user.id && post_id == post.id 514 // yeet the old cached like value 515 delete from LikeCache where post_id == post.id 516 } or { 517 eprintln('user ${user.id} failed to unlike post ${id}') 518 return ctx.server_error('failed to unlike post') 519 } 520 return ctx.ok('undisliked post') 521 } else { 522 // remove the old like, if it exists 523 if app.does_user_like_post(user.id, post.id) { 524 sql app.db { 525 delete from Like where user_id == user.id && post_id == post.id 526 } or { 527 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it') 528 } 529 } 530 531 like := Like{ 532 user_id: user.id 533 post_id: post.id 534 is_like: false 535 } 536 sql app.db { 537 insert like into Like 538 // yeet the old cached like value 539 delete from LikeCache where post_id == post.id 540 } or { 541 eprintln('user ${user.id} failed to dislike post ${id}') 542 return ctx.server_error('failed to dislike post') 543 } 544 return ctx.ok('disliked post') 545 } 546} 547 548@['/api/post/get_title'] 549fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result { 550 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') } 551 return ctx.text(post.title) 552} 553 554@['/api/post/edit'; post] 555fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result { 556 user := app.whoami(mut ctx) or { 557 ctx.error('not logged in!') 558 return ctx.redirect('/login') 559 } 560 post := app.get_post_by_id(id) or { 561 ctx.error('no such post') 562 return ctx.redirect('/') 563 } 564 if post.author_id != user.id { 565 ctx.error('insufficient permissions') 566 return ctx.redirect('/') 567 } 568 569 sql app.db { 570 update Post set body = body, title = title where id == id 571 } or { 572 eprintln('failed to update post') 573 ctx.error('failed to update post') 574 return ctx.redirect('/') 575 } 576 577 return ctx.redirect('/post/${id}') 578} 579 580////// site ////// 581 582@['/api/site/set_motd'; post] 583fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result { 584 user := app.whoami(mut ctx) or { 585 ctx.error('not logged in!') 586 return ctx.redirect('/login') 587 } 588 589 if user.admin { 590 sql app.db { 591 update Site set motd = motd where id == 1 592 } or { 593 ctx.error('failed to set motd') 594 eprintln('failed to set motd: ${motd}') 595 return ctx.redirect('/') 596 } 597 println('set motd to: ${motd}') 598 return ctx.redirect('/') 599 } else { 600 ctx.error('insufficient permissions!') 601 eprintln('insufficient perms to set motd to: ${motd} (${user.id})') 602 return ctx.redirect('/') 603 } 604}