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////// post ////// 322 323@['/api/post/new_post'; post] 324fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result { 325 user := app.whoami(mut ctx) or { 326 ctx.error('not logged in!') 327 return ctx.redirect('/') 328 } 329 330 if user.muted { 331 ctx.error('you are muted!') 332 return ctx.redirect('/me') 333 } 334 335 // validate title 336 if !app.validators.post_title.validate(title) { 337 ctx.error('invalid title') 338 return ctx.redirect('/me') 339 } 340 341 // validate body 342 if !app.validators.post_body.validate(body) { 343 ctx.error('invalid body') 344 return ctx.redirect('/me') 345 } 346 347 post := Post{ 348 author_id: user.id 349 title: title 350 body: body 351 } 352 353 sql app.db { 354 insert post into Post 355 } or { 356 ctx.error('failed to post!') 357 println('failed to post: ${post} from user ${user.id}') 358 return ctx.redirect('/me') 359 } 360 361 // find the post's id to process mentions with 362 if x := app.get_post_by_author_and_timestamp(user.id, post.posted_at) { 363 app.process_post_mentions(x) 364 } else { 365 ctx.error('failed to get_post_by_timestamp_and_author for ${post}') 366 } 367 368 return ctx.redirect('/me') 369} 370 371@['/api/post/delete'; post] 372fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result { 373 user := app.whoami(mut ctx) or { 374 ctx.error('not logged in!') 375 return ctx.redirect('/login') 376 } 377 378 post := app.get_post_by_id(id) or { 379 ctx.error('post does not exist') 380 return ctx.redirect('/') 381 } 382 383 if user.admin || user.id == post.author_id { 384 sql app.db { 385 delete from Post where id == id 386 delete from Like where post_id == id 387 } or { 388 ctx.error('failed to delete post') 389 eprintln('failed to delete post: ${id}') 390 return ctx.redirect('/') 391 } 392 println('deleted post: ${id}') 393 return ctx.redirect('/') 394 } else { 395 ctx.error('insufficient permissions!') 396 eprintln('insufficient perms to delete post: ${id} (${user.id})') 397 return ctx.redirect('/') 398 } 399} 400 401@['/api/post/like'] 402fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 403 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 404 405 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 406 407 if app.does_user_like_post(user.id, post.id) { 408 sql app.db { 409 delete from Like where user_id == user.id && post_id == post.id 410 // yeet the old cached like value 411 delete from LikeCache where post_id == post.id 412 } or { 413 eprintln('user ${user.id} failed to unlike post ${id}') 414 return ctx.server_error('failed to unlike post') 415 } 416 return ctx.ok('unliked post') 417 } else { 418 // remove the old dislike, if it exists 419 if app.does_user_dislike_post(user.id, post.id) { 420 sql app.db { 421 delete from Like where user_id == user.id && post_id == post.id 422 } or { 423 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it') 424 } 425 } 426 427 like := Like{ 428 user_id: user.id 429 post_id: post.id 430 is_like: true 431 } 432 sql app.db { 433 insert like into Like 434 // yeet the old cached like value 435 delete from LikeCache where post_id == post.id 436 } or { 437 eprintln('user ${user.id} failed to like post ${id}') 438 return ctx.server_error('failed to like post') 439 } 440 return ctx.ok('liked post') 441 } 442} 443 444@['/api/post/dislike'] 445fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 446 user := app.whoami(mut ctx) or { return ctx.unauthorized('not logged in') } 447 448 post := app.get_post_by_id(id) or { return ctx.server_error('post does not exist') } 449 450 if app.does_user_dislike_post(user.id, post.id) { 451 sql app.db { 452 delete from Like where user_id == user.id && post_id == post.id 453 // yeet the old cached like value 454 delete from LikeCache where post_id == post.id 455 } or { 456 eprintln('user ${user.id} failed to unlike post ${id}') 457 return ctx.server_error('failed to unlike post') 458 } 459 return ctx.ok('undisliked post') 460 } else { 461 // remove the old like, if it exists 462 if app.does_user_like_post(user.id, post.id) { 463 sql app.db { 464 delete from Like where user_id == user.id && post_id == post.id 465 } or { 466 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it') 467 } 468 } 469 470 like := Like{ 471 user_id: user.id 472 post_id: post.id 473 is_like: false 474 } 475 sql app.db { 476 insert like into Like 477 // yeet the old cached like value 478 delete from LikeCache where post_id == post.id 479 } or { 480 eprintln('user ${user.id} failed to dislike post ${id}') 481 return ctx.server_error('failed to dislike post') 482 } 483 return ctx.ok('disliked post') 484 } 485} 486 487@['/api/post/get_title'] 488fn (mut app App) api_post_get_title(mut ctx Context, id int) veb.Result { 489 post := app.get_post_by_id(id) or { return ctx.server_error('no such post') } 490 return ctx.text(post.title) 491} 492 493@['/api/post/edit'; post] 494fn (mut app App) api_post_edit(mut ctx Context, id int, title string, body string) veb.Result { 495 user := app.whoami(mut ctx) or { 496 ctx.error('not logged in!') 497 return ctx.redirect('/login') 498 } 499 post := app.get_post_by_id(id) or { 500 ctx.error('no such post') 501 return ctx.redirect('/') 502 } 503 if post.author_id != user.id { 504 ctx.error('insufficient permissions') 505 return ctx.redirect('/') 506 } 507 508 sql app.db { 509 update Post set body = body, title = title where id == id 510 } or { 511 eprintln('failed to update post') 512 ctx.error('failed to update post') 513 return ctx.redirect('/') 514 } 515 516 return ctx.redirect('/post/${id}') 517} 518 519////// site ////// 520 521@['/api/site/set_motd'; post] 522fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result { 523 user := app.whoami(mut ctx) or { 524 ctx.error('not logged in!') 525 return ctx.redirect('/login') 526 } 527 528 if user.admin { 529 sql app.db { 530 update Site set motd = motd where id == 1 531 } or { 532 ctx.error('failed to set motd') 533 eprintln('failed to set motd: ${motd}') 534 return ctx.redirect('/') 535 } 536 println('set motd to: ${motd}') 537 return ctx.redirect('/') 538 } else { 539 ctx.error('insufficient permissions!') 540 eprintln('insufficient perms to set motd to: ${motd} (${user.id})') 541 return ctx.redirect('/') 542 } 543}