a mini social media app for small communities
1module main 2 3import veb 4import auth 5import entity { Site, User, Post, Like, LikeCache } 6 7////// Users ////// 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 token := app.auth.add_token(x.id, ctx.ip()) or { 50 eprintln(err) 51 ctx.error('could not create token for user with id ${user.id}') 52 return ctx.redirect('/') 53 } 54 ctx.set_cookie( 55 name: 'token' 56 value: token 57 same_site: .same_site_none_mode 58 secure: true 59 path: '/' 60 ) 61 } else { 62 eprintln('could not log into newly-created user: ${user}') 63 ctx.error('could not log into newly-created user.') 64 } 65 66 return ctx.redirect('/') 67} 68 69@['/api/user/login'; post] 70fn (mut app App) api_user_login(mut ctx Context, username string, password string) veb.Result { 71 user := app.get_user_by_name(username) or { 72 ctx.error('invalid credentials') 73 return ctx.redirect('/login') 74 } 75 76 if !auth.compare_password_with_hash(password, user.password_salt, user.password) { 77 ctx.error('invalid credentials') 78 return ctx.redirect('/login') 79 } 80 81 token := app.auth.add_token(user.id, ctx.ip()) or { 82 eprintln('failed to add token on log in: ${err}') 83 ctx.error('could not create token for user with id ${user.id}') 84 return ctx.redirect('/login') 85 } 86 87 ctx.set_cookie( 88 name: 'token' 89 value: token 90 same_site: .same_site_none_mode 91 secure: true 92 path: '/' 93 ) 94 95 return ctx.redirect('/') 96} 97 98@['/api/user/logout'] 99fn (mut app App) api_user_logout(mut ctx Context) veb.Result { 100 if token := ctx.get_cookie('token') { 101 if user := app.get_user_by_token(ctx, token) { 102 app.auth.delete_tokens_for_ip(ctx.ip()) or { 103 eprintln('failed to yeet tokens for ${user.id} with ip ${ctx.ip()}') 104 return ctx.redirect('/login') 105 } 106 } else { 107 eprintln('failed to get user for token for logout') 108 } 109 } else { 110 eprintln('failed to get token cookie for logout') 111 } 112 113 ctx.set_cookie( 114 name: 'token' 115 value: '' 116 same_site: .same_site_none_mode 117 secure: true 118 path: '/' 119 ) 120 121 return ctx.redirect('/login') 122} 123 124@['/api/user/full_logout'] 125fn (mut app App) api_user_full_logout(mut ctx Context) veb.Result { 126 if token := ctx.get_cookie('token') { 127 if user := app.get_user_by_token(ctx, token) { 128 app.auth.delete_tokens_for_user(user.id) or { 129 eprintln('failed to yeet tokens for ${user.id}') 130 return ctx.redirect('/login') 131 } 132 } else { 133 eprintln('failed to get user for token for full_logout') 134 } 135 } else { 136 eprintln('failed to get token cookie for full_logout') 137 } 138 139 ctx.set_cookie( 140 name: 'token' 141 value: '' 142 same_site: .same_site_none_mode 143 secure: true 144 path: '/' 145 ) 146 147 return ctx.redirect('/login') 148} 149 150@['/api/user/set_nickname'; post] 151fn (mut app App) api_user_set_nickname(mut ctx Context, nickname string) veb.Result { 152 user := app.whoami(mut ctx) or { 153 ctx.error('you are not logged in!') 154 return ctx.redirect('/login') 155 } 156 157 mut clean_nickname := ?string(nickname.trim_space()) 158 if clean_nickname or { '' } == '' { 159 clean_nickname = none 160 } 161 162 // validate 163 if clean_nickname != none && !app.validators.nickname.validate(clean_nickname or { '' }) { 164 ctx.error('invalid nickname') 165 return ctx.redirect('/me') 166 } 167 168 sql app.db { 169 update User set nickname = clean_nickname where id == user.id 170 } or { 171 ctx.error('failed to change nickname') 172 eprintln('failed to update nickname for ${user} (${user.nickname} -> ${clean_nickname})') 173 return ctx.redirect('/me') 174 } 175 176 return ctx.redirect('/me') 177} 178 179@['/api/user/set_muted'; post] 180fn (mut app App) api_user_set_muted(mut ctx Context, muted bool) veb.Result { 181 user := app.whoami(mut ctx) or { 182 ctx.error('you are not logged in!') 183 return ctx.redirect('/login') 184 } 185 186 if user.admin || app.config.dev_mode { 187 sql app.db { 188 update User set muted = muted where id == user.id 189 } or { 190 ctx.error('failed to change mute status') 191 eprintln('failed to update mute status for ${user} (${user.muted} -> ${muted})') 192 return ctx.redirect('/user/${user.username}') 193 } 194 return ctx.redirect('/user/${user.username}') 195 } else { 196 ctx.error('insufficient permissions!') 197 eprintln('insufficient perms to update mute status for ${user} (${user.muted} -> ${muted})') 198 return ctx.redirect('/user/${user.username}') 199 } 200} 201 202@['/api/user/set_theme'; post] 203fn (mut app App) api_user_set_theme(mut ctx Context, url string) veb.Result { 204 if !app.config.instance.allow_changing_theme { 205 ctx.error('this instance disallows changing themes :(') 206 return ctx.redirect('/me') 207 } 208 209 user := app.whoami(mut ctx) or { 210 ctx.error('you are not logged in!') 211 return ctx.redirect('/login') 212 } 213 214 mut theme := ?string(none) 215 if url.trim_space() != '' { 216 theme = url.trim_space() 217 } 218 219 sql app.db { 220 update User set theme = theme where id == user.id 221 } or { 222 ctx.error('failed to change theme') 223 eprintln('failed to update theme for ${user} (${user.theme} -> ${theme})') 224 return ctx.redirect('/me') 225 } 226 227 return ctx.redirect('/me') 228} 229 230@['/api/user/set_pronouns'; post] 231fn (mut app App) api_user_set_pronouns(mut ctx Context, pronouns string) veb.Result { 232 user := app.whoami(mut ctx) or { 233 ctx.error('you are not logged in!') 234 return ctx.redirect('/login') 235 } 236 237 clean_pronouns := pronouns.trim_space() 238 if !app.validators.pronouns.validate(clean_pronouns) { 239 ctx.error('invalid pronouns') 240 return ctx.redirect('/me') 241 } 242 243 sql app.db { 244 update User set pronouns = clean_pronouns where id == user.id 245 } or { 246 ctx.error('failed to change pronouns') 247 eprintln('failed to update pronouns for ${user} (${user.pronouns} -> ${clean_pronouns})') 248 return ctx.redirect('/me') 249 } 250 251 return ctx.redirect('/me') 252} 253 254@['/api/user/set_bio'; post] 255fn (mut app App) api_user_set_bio(mut ctx Context, bio string) veb.Result { 256 user := app.whoami(mut ctx) or { 257 ctx.error('you are not logged in!') 258 return ctx.redirect('/login') 259 } 260 261 clean_bio := bio.trim_space() 262 if !app.validators.user_bio.validate(clean_bio) { 263 ctx.error('invalid bio') 264 return ctx.redirect('/me') 265 } 266 267 sql app.db { 268 update User set bio = clean_bio where id == user.id 269 } or { 270 ctx.error('failed to change bio') 271 eprintln('failed to update bio for ${user} (${user.bio} -> ${clean_bio})') 272 return ctx.redirect('/me') 273 } 274 275 return ctx.redirect('/me') 276} 277 278@['/api/user/get_name'] 279fn (mut app App) api_user_get_name(mut ctx Context, username string) veb.Result { 280 user := app.get_user_by_name(username) or { 281 return ctx.server_error('no such user') 282 } 283 return ctx.text(user.get_name()) 284} 285 286////// Posts ////// 287 288@['/api/post/new_post'; post] 289fn (mut app App) api_post_new_post(mut ctx Context, title string, body string) veb.Result { 290 user := app.whoami(mut ctx) or { 291 ctx.error('not logged in!') 292 return ctx.redirect('/') 293 } 294 295 if user.muted { 296 ctx.error('you are muted!') 297 return ctx.redirect('/me') 298 } 299 300 // validate title 301 if !app.validators.post_title.validate(title) { 302 ctx.error('invalid title') 303 return ctx.redirect('/me') 304 } 305 306 // validate body 307 if !app.validators.post_body.validate(body) { 308 ctx.error('invalid body') 309 return ctx.redirect('/me') 310 } 311 312 post := Post{ 313 author_id: user.id 314 title: title 315 body: body 316 } 317 318 sql app.db { 319 insert post into Post 320 } or { 321 ctx.error('failed to post!') 322 println('failed to post: ${post} from user ${user.id}') 323 return ctx.redirect('/me') 324 } 325 326 return ctx.redirect('/me') 327} 328 329@['/api/post/delete'; post] 330fn (mut app App) api_post_delete(mut ctx Context, id int) veb.Result { 331 user := app.whoami(mut ctx) or { 332 ctx.error('not logged in!') 333 return ctx.redirect('/login') 334 } 335 336 post := app.get_post_by_id(id) or { 337 ctx.error('post does not exist') 338 return ctx.redirect('/') 339 } 340 341 if user.admin || user.id == post.author_id { 342 sql app.db { 343 delete from Post where id == id 344 delete from Like where post_id == id 345 } or { 346 ctx.error('failed to delete post') 347 eprintln('failed to delete post: ${id}') 348 return ctx.redirect('/') 349 } 350 println('deleted post: ${id}') 351 return ctx.redirect('/') 352 } else { 353 ctx.error('insufficient permissions!') 354 eprintln('insufficient perms to delete post: ${id} (${user.id})') 355 return ctx.redirect('/') 356 } 357} 358 359@['/api/post/like'] 360fn (mut app App) api_post_like(mut ctx Context, id int) veb.Result { 361 user := app.whoami(mut ctx) or { 362 return ctx.unauthorized('not logged in') 363 } 364 365 post := app.get_post_by_id(id) or { 366 return ctx.server_error('post does not exist') 367 } 368 369 if app.does_user_like_post(user.id, post.id) { 370 sql app.db { 371 delete from Like where user_id == user.id && post_id == post.id 372 // yeet the old cached like value 373 delete from LikeCache where post_id == post.id 374 } or { 375 eprintln('user ${user.id} failed to unlike post ${id}') 376 return ctx.server_error('failed to unlike post') 377 } 378 return ctx.ok('unliked post') 379 } else { 380 // remove the old dislike, if it exists 381 if app.does_user_dislike_post(user.id, post.id) { 382 sql app.db { 383 delete from Like where user_id == user.id && post_id == post.id 384 } or { 385 eprintln('user ${user.id} failed to remove dislike on post ${id} when liking it') 386 } 387 } 388 389 like := Like{ 390 user_id: user.id 391 post_id: post.id 392 is_like: true 393 } 394 sql app.db { 395 insert like into Like 396 // yeet the old cached like value 397 delete from LikeCache where post_id == post.id 398 } or { 399 eprintln('user ${user.id} failed to like post ${id}') 400 return ctx.server_error('failed to like post') 401 } 402 return ctx.ok('liked post') 403 } 404} 405 406@['/api/post/dislike'] 407fn (mut app App) api_post_dislike(mut ctx Context, id int) veb.Result { 408 user := app.whoami(mut ctx) or { 409 return ctx.unauthorized('not logged in') 410 } 411 412 post := app.get_post_by_id(id) or { 413 return ctx.server_error('post does not exist') 414 } 415 416 if app.does_user_dislike_post(user.id, post.id) { 417 sql app.db { 418 delete from Like where user_id == user.id && post_id == post.id 419 // yeet the old cached like value 420 delete from LikeCache where post_id == post.id 421 } or { 422 eprintln('user ${user.id} failed to unlike post ${id}') 423 return ctx.server_error('failed to unlike post') 424 } 425 return ctx.ok('undisliked post') 426 } else { 427 // remove the old like, if it exists 428 if app.does_user_like_post(user.id, post.id) { 429 sql app.db { 430 delete from Like where user_id == user.id && post_id == post.id 431 } or { 432 eprintln('user ${user.id} failed to remove like on post ${id} when disliking it') 433 } 434 } 435 436 like := Like{ 437 user_id: user.id 438 post_id: post.id 439 is_like: false 440 } 441 sql app.db { 442 insert like into Like 443 // yeet the old cached like value 444 delete from LikeCache where post_id == post.id 445 } or { 446 eprintln('user ${user.id} failed to dislike post ${id}') 447 return ctx.server_error('failed to dislike post') 448 } 449 return ctx.ok('disliked post') 450 } 451} 452 453////// Site ////// 454 455@['/api/site/set_motd'; post] 456fn (mut app App) api_site_set_motd(mut ctx Context, motd string) veb.Result { 457 user := app.whoami(mut ctx) or { 458 ctx.error('not logged in!') 459 return ctx.redirect('/login') 460 } 461 462 if user.admin { 463 sql app.db { 464 update Site set motd = motd where id == 1 465 } or { 466 ctx.error('failed to set motd') 467 eprintln('failed to set motd: ${motd}') 468 return ctx.redirect('/') 469 } 470 println('set motd to: ${motd}') 471 return ctx.redirect('/') 472 } else { 473 ctx.error('insufficient permissions!') 474 eprintln('insufficient perms to set motd to: ${motd} (${user.id})') 475 return ctx.redirect('/') 476 } 477}